Algo for Auto-Suggesting Wallpaper Resolutions Based on Aspect Ratio – Cloudinary & React

Algo for Auto-Suggesting Wallpaper Resolutions Based on Aspect Ratio – Cloudinary & React

Cloudinary is a wonderful platform that allows images and videos hosting for free. Beside this, it allows user to make any sort of image transformation right through their delivery URL. One such transformation allows you to change the resolution of an image.

It’s easy, add a string mentioning width and height at a certain position and you will be able to do it dynamically.

For eg: – In order to resize a 4K image to 1280 x 720 px resolution, add w_1280,h_720 after /upload/. As in the URL below.

https://res.cloudinary.com/cloudofrahul/image/upload/w_1280,h_720/v1739183549/wallpaper/Broken%20Anime%20Android%20Girl.jpg

You are free to change it to any resolution you may like.

Take a look below, it’s an image from one of my favorite wallpaper websites. It offers multiple resolution options all lower than the original. Technically, you could just download the highest one since the internet speed is not a concern anymore. But it’s still useful to have alternatives.

How do You write a code to Automated Image Scaling using Aspect Ratio?

The answer is simple yet creative.

Using binary search.

In order to write an algo we first determine the aspect ratio of given image.
After determining it, we will suggest lower resolution based on that aspect ratio using Binary Search.
Then, we will manipulate Cloudinary deliver URL to fetch the image of those resolutions.

Before I start, I would like to mention that this code is from my Wallpaper Website Project. You can find it hosted on GitHub, here’s the link.

So, let get started one by one.

Determining the Aspect Ratio of Image

We are working on a file “ResizeComponent.jsx” in this step.
This is a component that receives the width, height, and the url of our image.

The first order of the business is to create an aspect ratio dataset that contains 3 properties.

ratios, title, and resolutions

aspectRatioData object is in a separate file “common_res.js”

export const aspectRatioData = {
    "ap_16_9": {
        ratios: [16 / 9],
        title: "16x9",
        resolutions: [
            {2160: "w_3840,h_2160"},
            {1440: "w_2560,h_1440"},
            {1080:"w_1920,h_1080"},
            {900: "w_1600,h_900"},
            {768: "w_1366,h_768"},
            {720:"w_1280,h_720"},
            {450:"w_800,h_450"}
        ],
    },
    "ap_21_9": {
        ratios: [21 / 9, 2.37037, 2.38889, 2.4],
        title: "21x9",
        resolutions: [
            {2880:"w_6880,h_2880"},
            {2160: "w_5120,h_2160"},
            {1440: "w_3440,h_1440"},
            {1080:"w_2560,h_1080"},
        ]
    },
};

// more resolution available here
// https://wallpaperswide.com/cute_kitten_close_up-wallpapers.html

I omitted the other resolutions to make it convenient. Both 16:9 and 21:9 will convey what I am trying to do in my code.

Unlike 16:9 or any other aspect ratio, 21:9 resolutions quotients are not constant. Otherwise, I would not be putting ratios inside an array. You can try dividing width of 21:9 resolutions by their heights. You can have a look at this webpage to get more information on 21:9 Widescreen resolution.

You can find more aspect ratios on WallpapersWide.

Please note I am coding this inside useEffect, so let’s have a skeleton of it first.

  useEffect(() => {
    const determineAspectRatio = async () => {
     ....
}
    determineAspectRatio()
  }, [width, height]);

Since I am working a website that stores Desktop wallpapers, the image height must be lower than its width. If it is not the case, image resolution is vertical.

      if (width < height) {
        return;
      }

Next, I divide the width by its height and store it in a constant, div.

      const div = width / height;

Say, for 1920×1080, I will store the value 1.777777777777778 into div.
The idea is, I will get into the array and find the ratios with the similar value. ap_16_9 ratio entry will have the similar value.
Notice that I am using the word similar, since I am giving a small margin to match those values. Some images could have resolutions like 1919×1080.

      for (const [key, data] of Object.entries(aspectRatioData)) {
        if (data.ratios.some(r=> checkAspectRatio(div, r))) {
          console.log('aspectRatio key is', key )
          setAspectRatio(key);
        }
      }

checkAspectRatio is a function that help me determine the ratio. It is as simple as it could be.

Before this, let’s see what what is.
r is the absolute quotient of 16/9 and div is width/height. It may or may not give us what we are expecting as I already mentioned that an image could be of 1919×1080 dimension.

So, our checkAspectRatio function will check that our round-about value of ratio (div) – calculation (r) should be comparable.

checkAspectRatio function is in a separate file “checkAp.js”

export default function checkAspectRatio(ratio, calculation) {
    return Math.abs(ratio - calculation) < 0.005;
}

Once it confirms, we will get the aspect ratio. In our example, ap_16_9. We will store this entry to use state aspectRatio.

const [aspectRatio, setAspectRatio] = useState(null);

Finding the Index of Next Possible Resolution

For this and the next step, we will be working on a file “ResizeImage.jsx“.
This is a component that receives the url, height, and the ap from “ResizeComponent.jsx”.

In this step, our objective is to find the index of resolution that is lower than the current. Our base will be the aspect ratio we have determined in the previous step.
Take a look at the image below. We will achieve this in the next step.

These are all the resolutions I have listed in aspectRatioData for ap_16_9

These are the use state we will need in this and the next step.

    const [resizeObj, setResizeObj] = useState([])
    const [aspectRatioTitle, setAspectRatioTitle] = useState(undefined)
    const [noRes, setNoRes] = useState(false)

resizeObj will display all the available resolution lower the image resolution.
aspectRatioTitle is the 16×9 in our image above. It will hold the title that we have in aspectRatioData.
noRes would be true if we failed to match aspect ratio with aspectRatioData.

Please note I am coding this inside useEffect, so let’s have a skeleton of it first.

    useEffect(() => {
        async function fetchPossibleResolutions () {
          ...
        }
        fetchPossibleResolutions()
    }, [url, height, ap]);

In this step, our first order of business will be handling the edge case where we don’t have the aspect ratio.

            if (!ap){
                console.log('value of ap is null', ap)
                setNoRes(true);
                return;
            }

            let fetchedAp = aspectRatioData[ap] || null;
            let selectedRes = ap || null;
            if (!fetchedAp) {
                console.log('feetched ap value seems to be null.......')
                setNoRes(true);
                return;
            }

Then we determine the bestMatchIndex.

            const bestMatchIndex = findResolution(fetchedAp, height)

Take an another look at the picture above, you will find that I have provided suggestion for resolutions lower than the actual image size.
I also mentioned that resizeObj is doing something like that.

What resizeObj is doing is this.

setResizeObj(fetchedAp.resolutions.slice(bestMatchIndex))

We will fetch the index after our wallpaper resolution. Say, for 1920×1080 we will fetch the index of 1600×900.
So, let’s get back to our findResolution method.

findResolution function is in a separate file “findResolution.js”

export default function findResolution(common_res, height) {
    ...
}

The common_res is the fetchedAp. You can rename it if you want to, height is the height of the image.

This is our fetchedAp.

   "ap_16_9": {
        ratios: [16 / 9],
        title: "16x9",
        resolutions: [
            {2160: "w_3840,h_2160"},
            {1440: "w_2560,h_1440"},
            {1080:"w_1920,h_1080"},
            {900: "w_1600,h_900"},
            {768: "w_1366,h_768"},
            {720:"w_1280,h_720"},
            {450:"w_800,h_450"}
        ],
    },
    const resolutions = common_res.resolutions;
    if (!resolutions || resolutions.length === 0) return null;

    const keys = resolutions.map(obj => parseInt(Object.keys(obj)[0]));
    const n = keys.length;

We are storing the resolutions array from common_res in resolutions constant.

keys = [2160, 1440, 1080, 900, 768, 720, 450]
// this is not the code but the display of keys

If we don’t find the keys, we will return null.

    if (height < keys[n - 1]) {
        return null;
    }

Otherwise, we will run a binary search on the array.

    let left = 0, right = n - 1;
    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        if (keys[mid] === height) {
            return mid + 1;
        } else if (keys[mid] > height) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }

Remember, we have a sorted array, and we are going in descending order.
The left represents the index of highest resolution, and the right represent the index of lowest resolution.

If keys[mid] matches with 1080. We will return the index of 1080 + 1, that means we will return the index of 1600.
Remember, mid represent the middle index.

If the code failed to return anything, we would simply return the index of left.

return left < n ? left : null;

Resizing the Image

Once we find the bestMatchIndex its time we should complete our code and use Cloudinary delivery URL to resize the image.

Consider the edge cases first.

            if (!bestMatchIndex) {
                console.log('bestMatchIndex value does not exist', bestMatchIndex)
                setNoRes(true);
                return;
            }

            if (bestMatchIndex === fetchedAp.resolutions.length) {
                setNoRes(true);
                console.log('last resolution')
            }

Now its time to setResizeObj.

            const apTitle = aspectRatioData[ap]?.title || null;
            setAspectRatioTitle(apTitle);
            setResizeObj(fetchedAp.resolutions.slice(bestMatchIndex))

We did two things in the code above.
We find the apTitle to display it on our website and store it in aspectRatioTitle state.
We slice the resolutions array with the bestMatchIndex and stored it in resizeObj.

Let’s finalize the code by adding it frontend.

If incase our noRes is true we will return this.

    if (noRes){
        return (
                <p className="text-sm font-semibold">No resize options available. This seems to be the lowest resolution available for this wallpaper</p>
    )

    }

Otherwise,

{resizeObj.map(
    obj => Object.entries(obj).map(([_, value]) => (
        <li key={_} className="text-blue-800 hover:underline cursor-pointer font-semibold bg-amber-500 p-1 hover:bg-amber-800 hover:text-white">
            <a target="_blank" href={url.replace('/upload/', `/upload/${value}/`)}>{value.replace('w_', "").replace("h_", "x").replace(",", "")}
            </a>
            </li>
    ))
)}

url is Cloudinary delivery URL. It could be this,

https://res.cloudinary.com/cloudofrahul/image/upload/w_1280,h_720/v1739183549/wallpaper/Broken%20Anime%20Android%20Girl.jpg

This marks my third article on Cloudinary series.

So far, I have covered.

Single file upload through frontend (link).
Single file upload through backend, ExpressJs on my dev.to blog (link).

I would add more article in this series. For now, let me know if this code helped you in any way.
If you need any help or suggest any alternative, please use the comment section below. I would love to answer your question and get to know different approaches to achieve the same.

For now, have a nice day!

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *