The Secret to Sharper Images 🥷🏻: Solving the Export Quality Problem
Using ImageMagick to export sharper images
😀 Introduction
When it comes to advertising, every detail matters. From captivating headlines to captivating images, advertisers constantly strive to create eye-catching ad creatives that leave a lasting impact on their audience. Among the many elements at play, two often overlooked yet crucial factors are the colors used and the sharpness of text within the creative.
“Crisp and sharp text is the anchor that holds an ad together. It ensures that the message cuts through the noise and makes a lasting impact on the viewer” - David Ogilvy, advertising legend and founder of Ogilvy & Mather.
Picture this: You're scrolling through your social media feed, and amidst a sea of blurry or pixelated ads, one stands out with crisp, razor-sharp text. Your eyes are instantly drawn to it, and within seconds, you've grasped its message. That's the power of sharp text – it grabs attention, communicates effectively, and ensures the viewer doesn't miss out on the intended message.
In this blog, we will discuss everyday issues we face when creating ad creatives from any web-based platform like Rocketium, Canva, or Photopea.
🌫️ Blurry Export: The Problem
Before jumping on to the issue, let’s discuss how to export work in Rocketium and how we generate images from HTML.
Any idea how to do it?
If your answer was to use Puppeteer and take a screenshot, Then we all are on the same page, if haven’t guessed it correctly. Let’s discuss how it works. At Rocketium we handle thousands of creatives that too with different file types, being a web-based platform we use Puppeteer to generate creatives. Under the hood, it opens a headless Chrome and takes a screenshot of the HTML output, and exports it in the required file format. So with this, we can generate anything on HTML and simply opens up a new headless Chrome tab and then take a screenshot there.
📸 When it comes to taking screenshots using Puppeteer, here are some major hurdles you may encounter:
Rendering Inconsistencies: One of the primary challenges with Puppeteer is dealing with rendering inconsistencies across different websites and web applications. Since Puppeteer relies on the Chromium browser, variations in CSS styles, JavaScript behavior, or dynamic content can result in screenshots that differ from what is expected. Ensuring consistent and accurate rendering across various scenarios can be a complex task.
Page Load Timing: Capturing screenshots at the precise moment when a page has finished loading its content is crucial for obtaining accurate representations of web pages. However, timing can be challenging, especially when dealing with pages that load dynamically or have asynchronous content. Ensuring that Puppeteer waits for all necessary resources to load before taking a screenshot requires careful handling and synchronization.
Performance and Resource Usage: Puppeteer operates by launching a headless Chromium browser, which consumes significant system resources. This can impact performance, especially when dealing with complex or resource-intensive web pages. Efficiently managing resources, optimizing code, and handling potential memory leaks are essential to maintain a smooth and reliable screenshot-capturing process.
Image Size and Resolution: The resolution and size of the captured screenshot can significantly impact its clarity. Puppeteer allows you to specify the viewport size and the dimensions of the screenshot, and using appropriate values is crucial for obtaining sharp and clear images. Inadequate resolution or incorrectly sized screenshots can result in blurry output.
Sharpness: The preview of any HTML Page depends totally upon the user screen, so while generating it totally depends upon which OS and machine you are using for your servers. This generates a lot of issues with the quality of images in the creative.
We have handled most of the issues and tackled them while setting up the infrastructure but recently we hit a bottleneck with blurry outputs.
While working with one of the customers, a customer reported that one of the product images is very blurry post output and when we dug deep, Puppeteer was at fault 🤯 .
We tried looking at the issue and the root cause was the screenshot. Till the screenshot the creative looked fine but when we take the screenshot the Image lose the sharpness. So we thought of doing some experiments to resolve this. Let’s look into what we tried.
🧪 Experiments
To resolve this issue, we spent 3 weeks trying different experiments to figure out if we can achieve some success.
🟡 - Improvement, 🔴 - No Improvement, 🟢 - Success
Some of the approaches we have tried -
🟡 Scaling the preview to twice the size and resizing to the original
🔴 Taking a manual screen grab on Mac.
🔴 Taking a manual screen grab on Windows.
🔴 Taking a screenshot using Puppeteer on Mac.
🔴 Taking a screenshot using Chrome API.
🔴 Taking a screenshot on a GPU machine.
🔴 Taking a screenshot after adding CSS options to instruct the browser to render sharp.
🔴 CSS -
image-rendering: -webkit-optimize-contrast;
🔴 CSS -
transform: translateZ(1px) OR transform: translateZ(0);
🔴 ‘srcset’ instead of src in <img> tag.
🔴 Resize the product image to be the exact size it is supposed to be rendered first and then use the URL.
🔴 Sharpen the image before and add that and then generate output.
🔴 Run puppeteer on Mac M1 Air and Mac mini.
After trying all the experiments, we still couldn’t achieve the results. We were out of ideas. We had literally no idea what to do here, then Santosh ( VP at Engineering ) pitched to research around how Photoshop is able to achieve this whereas none of the web-based platforms are able to solve this problem.
🔬 The Research: First Step Towards Success
When we started researching how PhotoShop generates images, the first thing that we could sense was, PS generated everything client side, and that to pixel by pixel impainting. Using the user’s device power and capabilities it generated each creative by imprinting each pixel instead of a screenshot. Not only this then we came across a research paper and an article mentioning how sharpening is used post-export in various platforms and even in PS.
My first reaction to this article was, this was so obvious! If you have blurry output, instead of fixing the export, sharpen the export and TADA! As soon as this struck, we all had a ray of hope and an idea of what can be done here. We started reading articles about different techniques and algorithms that sharpen an Image, and believe me when I say there are 100s of algo, there are actually so many of them that we were so confused about which one to use and why? We read about each one of them ( Like not every but at least the top 10 ). Then we decided to look out for the options that we can integrate easily and solves for our use case!
Then we came across this Article, which explained how resizing works and how an image's brightness/blurry-ness happens. So we started digging more into it and then did some tests. There was so much to handle but finally, we hit the jackpot. We could see the difference
🔑Solution: The Final Approach
So now it was clear that sharpening will do the magic. So we finalized with ‘Image Magick’. We started looking at how Image Magic works and what algorithms it works on.
So we came across a command line tool from ImageMagick itself that will do this job for us. So we tried our hands on it and it had so many properties that we could play around with.
Let’s jump into some code and figure out how Image Magick works.
This is the ImageMagick command that does the magic (sharpening) for us.
magick sample.jpeg -unsharp 1.5x1+0.7+0.02 sharpened.jpeg
So here we can see there are multiple parameters that we need to pass on. Let’s look into it what all these parameters stand for.
So here unsharp is the main command that enables the sharpening algo. After that, we have the parameters. There are 4 values that determine how the logic will work and what it will change for colors and borders to make the image sharper.
Let’s Focus on these 4 parameters.
Radius (1.5) - The radius of the Gaussian, in pixels, not counting the center pixel (default 0).
Sigma (1) - The standard deviation of the Gaussian, in pixels (default 1.0).
Gain (0.7) - The fraction of the difference between the original and the blurred image that is added back into the original (default 1.0).
Threshold (0.02) - The threshold, as a fraction of QuantumRange, is needed to apply the difference amount (default 0.05).
So we can manipulate these values and can identify which works better for which use case. For Our use came up with these values.
🚀 Implementation
Our export works mostly on serverless so we had to install ImageMagick there and use it in code. So we installed it using a layer under GM and used it as a function where we can pass the values from the frontend so that if in the future we want to manipulate we can easily play with the values from the frontend itself.
So finally we were able to implement and sharpen images as per the requirements.
You can refer to the above function written in javascript :)
Everyone was happy and we were about to ship this as well, but it wasn’t this easy as well. While testing we came across some cases which just took our whole implementation to 2 steps back. We were sharpening the whole screenshot and exporting it but due to a major issue raised that even the text and the background image were sharpened we somehow ended up creating a jaggy effect for the text.
😱 The Climax
So after facing issues with the text, we decided to take another step and make this a bit more adaptable for our use case. So now after taking screenshots of the whole creation, we wrote some rules to figure out which all images need to be sharpened. We hide all other elements and just keep that image and take another screenshot of it and then sharpen it. Example -
And then we stitch this image back over the main creative. That should solve the issue, right? NO 😖
With this another issue comes that was what if there is an image above a text? This would overlap text. So now we sat back and finally came up with the final approach.
So now we take each element of the creative, hide other elements, and take a screenshot of it. So if we have 5 layers, we have 5 screenshots ready with us, with the rules we take this array and replace these images with sharpened images ( for all which are required ). After all, the sharpening is done, we send this to a function that takes all the images and stitches each of them over each other.
And TADA! We are finally done with our implementation and now you can easily get all the sharpened export even if Puppeteer doesn’t let you :)
This is pure hard-work and research put into action methodically, and loved knowing more about color sharpening and how internally these design tools does work.
Eager to see the next one from you.
Well explained!! Nice 🧑💻