Issues

Automatically Creating Thumbnail PDFs in Umbraco

In this article I will show you how to automatically create a thumbnail for PDFs which are uploaded to the media section in the Umbraco backoffice.

This came about due to a website which was upgraded by Moriyama from version 7 to version 10 needing this functionality to be migrated over.

The functionality which is described in this article has been packaged up into three free to use packages on NuGet, the first one relies on Ghostscript, the second one relies on ABCpdf and the third one relies on IronPDF

To solve this issue I made use of the MediaSavingNotification notification handler to intercept the file upload process and before the media item is saved to generate a PNG thumbnail image, which is then added to a thumbnail property on the Article media type.

Backoffice configuration

Before we can make a start on the notification handler the Article media type needs to have an additional property called Thumbnail added, this should use the Upload property editor and have an alias of thumbnail.

Next reorder the property editors so that Thumbnail is at position 0, this will mean that when you are viewing the PDF in the media section it will use the generated thumbnail instead of the PDF icon.

 

Dependencies

Depending on your licensing requirements, Moriyama has created three packages. Once is dependant on the commercial ABCpdf library and the other makes use of the Ghostscript library.

You can find the packages on NuGet here:

Moriyama.PreviewPDF.Ghostscript

Moriyama.PreviewPDF.ABCpdf

Moriyama.PreviewPDF.IronPDF

You can find the source code on GitHub here:

https://github.com/Moriyama-Umbraco/Moriyama.PreviewPDF

Notification Handler

There are a wide range of notifications available in Umbraco, these can be used to intercept most events. You can find documentation on all of the available events in the Umbraco Documentation.

We will be using the MediaSavingNotification to add the generated thumbnail as part of the PDF upload.

The MediaSavingNotification is fired after the media item Save request has been made, but before the media item is saved to the filesystem and database, this meant it was a perfect place to modify the request and insert the thumbnail.

You can see the code in the two versions of the Notification Handler below, the flow is as follows:

  • Check is the media type is of Article
  • Check there is no value for the PdfFileAlias property
  • Check the file extension ends in .pdf

GhostScript Specific

  • Find the bin path
  • Load the correct DLL for the environment (64 or 32 Bit)
  • Read the PDF and load the first page into a MemoryStream
  • Create a new MemoryStream containing a PNG of the first page of the PDF
  • Update the Article media entity Thumbnail property with the image Stream

Due to the thumbnail being tied to the media item, when you are handling the frontend rendering, you can simply output the thumbnail using the .Value extension as normal, you can also pass the image through ImageSharp if you wish to use that to crop / resize the image where required in the usual fashion.

You can read about that on the Umbraco documentation site here: https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/image-cropper#powered-by-imagesharp.web

You can see each of the notification handlers below:

Moriyama.PreviewPDF.Ghostscript

PdfPreviewNotificationHandler.cs

https://github.com/Moriyama-Umbraco/Moriyama.PreviewPDF/blob/main/Moriyama.PreviewPDF.Ghostscript/NotificationHandlers/PdfPreviewNotificationHandler.cs

public void Handle(MediaSavingNotification notification)
{
    foreach (var entity in notification.SavedEntities)
    {
        if (entity.ContentType.Alias.Equals(Umbraco.Cms.Core.Constants.Conventions.MediaTypes.ArticleAlias))
        {
            var filePropertyValue = (string?)entity.GetValue(_configuration.PdfFileAlias);
            if (!string.IsNullOrWhiteSpace(filePropertyValue))
            {
                var isPdf = filePropertyValue.EndsWith(MoriyamaPreviewPDFConstants.PdfExtension, StringComparison.InvariantCultureIgnoreCase);

                if (isPdf)
                {
                    try
                    {
                        _logger.LogInformation("Starting PDF thumbnail creation");
                        var binPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
                        var gsDllPath = Path.Combine(binPath, Environment.Is64BitProcess ? MoriyamaPreviewPDFConstants.GhostscriptDll64 : MoriyamaPreviewPDFConstants.GhostscriptDll32);
                        var version = new GhostscriptVersionInfo(new Version(10, 02, 1), gsDllPath, string.Empty, GhostscriptLicense.GPL);

                        using (var rasterizer = new GhostscriptRasterizer())
                        {
                            var file = _mediaFileManager.GetFile(entity, out _);
                            rasterizer.Open(file, version, true);
                            var firstPageAsImage = rasterizer.GetPage(200, _configuration.PdfPageNumber);

                            using (var memoryStream = new MemoryStream())
                            {
                                _logger.LogInformation("Creating PNG thumbnail and saving to memory stream");
                                firstPageAsImage.Save(memoryStream, ImageFormat.Png);
                                memoryStream.Position = 0;

                                _logger.LogInformation("Updating entity with thumbnail before saving");
                                entity.SetValue(_mediaFileManager, _mediaUrlGeneratorCollection, _shortStringHelper, _contentTypeBaseServiceProvider, _configuration.ThumbnailAlias, _configuration.ThumbnailFileName, memoryStream);
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, ex.Message);
                    }
                }
            }
        }
    }
}

Moriyama.PreviewPDF.ABCpdf

PdfPreviewNotificationHandler.cs

https://github.com/Moriyama-Umbraco/Moriyama.PreviewPDF/blob/main/Moriyama.PreviewPDF.ABCpdf/NotificationHandlers/PdfPreviewNotificationHandler.cs

public void Handle(MediaSavingNotification notification)
{
    foreach (var entity in notification.SavedEntities)
    {
        if (entity.ContentType.Alias.Equals(Umbraco.Cms.Core.Constants.Conventions.MediaTypes.ArticleAlias))
        {
            var filePropertyValue = (string?)entity.GetValue(_configuration.PdfFileAlias);
            if (!string.IsNullOrWhiteSpace(filePropertyValue))
            {
                var isPdf = filePropertyValue.EndsWith(MoriyamaPreviewPDFConstants.PdfExtension, StringComparison.InvariantCultureIgnoreCase);

                if (isPdf)
                {
                    try
                    {
                        _logger.LogInformation("Starting PDF thumbnail creation");
                        Doc doc = new Doc();

                        _logger.LogInformation("Getting PDF from file manager");
                        doc.Read(_mediaFileManager.GetFile(entity, out _));

                        doc.PageNumber = 1;
                        doc.Rect.String = doc.CropBox.String;
                        using (var memoryStream = new MemoryStream())
                        {
                            _logger.LogInformation("Rendering page 1 of PDF");
                            var thumbnail = doc.Rendering.GetBitmap();

                            _logger.LogInformation("Creating PNG thumbnail and saving to memory stream");
                            thumbnail.Save(memoryStream, ImageFormat.Png);
                            memoryStream.Position = 0;

                            _logger.LogInformation("Updating entity with thumbnail before saving");
                            entity.SetValue(_mediaFileManager, _mediaUrlGeneratorCollection, _shortStringHelper, _contentTypeBaseServiceProvider, _configuration.ThumbnailAlias, _configuration.ThumbnailFileName, memoryStream);
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, ex.Message);
                    }
                }
            }
        }
    }
}

If you choose to use the ABCpdf library you will need to exclude some types from the TypeFinder, you can read more about the setting on GitHub here.

To do this you simply add or update the following appsetting in your appsettings.json file:

{
    "Umbraco": {
        "CMS": {
            "TypeFinder": {
            "AssembliesAcceptingLoadExceptions": "*",
            "AdditionalAssemblyExclusionEntries": [ "WindowsBase", "PresentationFramework", "ReachFramework", "PresentationCore" ]
            }
        }
    }
}

Moriyama.PreviewPDF.IronPDF

PdfPreviewNotificationHandler.cs

https://github.com/Moriyama-Umbraco/Moriyama.PreviewPDF/blob/main/Moriyama.PreviewPDF.IronPDF/NotificationHandlers/PdfPreviewNotificationHandler.cs

public void Handle(MediaSavingNotification notification)
{
    foreach (var entity in notification.SavedEntities)
    {
        if (entity.ContentType.Alias.Equals(Umbraco.Cms.Core.Constants.Conventions.MediaTypes.ArticleAlias))
        {
            var filePropertyValue = (string?)entity.GetValue(_configuration.PdfFileAlias);
            if (!string.IsNullOrWhiteSpace(filePropertyValue))
            {
                var isPdf = filePropertyValue.EndsWith(MoriyamaPreviewPDFConstants.PdfExtension, StringComparison.InvariantCultureIgnoreCase);

                if (isPdf)
                {
                    try
                    {
                        _logger.LogInformation("Starting PDF thumbnail creation");
                        _logger.LogInformation("Getting PDF from file manager");

                        using (var pdf = new PdfDocument(_mediaFileManager.GetFile(entity, out _)))
                        {

                            using (var memoryStream = new MemoryStream())
                            {
                                _logger.LogInformation("Rendering page 1 of PDF and creating PNG thumbnail and saving to memory stream");
                                pdf.PageToBitmap(1).ExportStream(memoryStream, AnyBitmap.ImageFormat.Png);
                                memoryStream.Position = 0;

                                _logger.LogInformation("Updating entity with thumbnail before saving");
                                entity.SetValue(_mediaFileManager, _mediaUrlGeneratorCollection, _shortStringHelper, _contentTypeBaseServiceProvider, _configuration.ThumbnailAlias, _configuration.ThumbnailFileName, memoryStream);
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, ex.Message);
                    }
                }
            }
        }
    }
}

If you choose to use the IronPDF library you will need to exclude some types from the TypeFinder, you can read more about the setting on GitHub here.

To do this you simply add or update the following appsetting in your appsettings.json file:

{
    "Umbraco": {
        "CMS": {
            "TypeFinder": {
                "AssembliesAcceptingLoadExceptions": "*",
                "AdditionalAssemblyExclusionEntries": [
                    "SkiaSharp",
                    "PdfToSvg",
                    "Ninject",
                    "ZXing.ImageSharp.V2",
                    "zxing",
                    "DocumentFormat.OpenXml",
                    "OpenXmlPowerTools",
                    "Markdig.Signed",
                    "RtfPipe",
                    "DotNetZip",
                    "Babel.Licensing",
                    "Azure.Data.Tables",
                    "DeviceId.Windows",
                    "DeviceId.Linux",
                    "DeviceId.Mac",
                    "DeviceId",
                    "BouncyCastle.Crypto"
                ]
                }
        }
    }
}

Licensing

There is a split license in place for Ghostscript, the open source version is covered by an AGPL license, this means you must share the source code where it is used.

They offer a commercial license which allows for closed source use, you can read about that here.

The ABCpdf library is fully licensed, you can however use it in development on localhost without a license. You can read more about the different licenses on their website here.

The IronPDF library is fully licensed, you can however use it in development on localhost without a license. You can read more about the different licenses on their website here.

Final Comments

This small change can make a huge difference to the editors flow by removing the need to manually create an image for each uploaded PDF.

Aaron Sadler

Aaron is a .NET developer, with over 10 years experience in the industry, and 5 years working with Azure. He likes to create plugins and give back to the Umbraco community, and keeping up to date with the industry technologies. Outside of work, he likes to fiddle with his car, and has a young family which keeps him busy.

comments powered by Disqus