I’m working on a random project at the moment – I won’t go into details at the moment – but the basic building blocks are ReactJS + Cloudinary + JQuery + some other stuff. Everything was going smoothly until I got to implementing a feature that is integral to the project – “chaining” the output of a Cloudinary remote fetch as a transformation overlay on top of another image. In particular, I wanted to use the core Javascript library functions to do this, as opposed to their React SDK (I’m not even sure if it would be possible with just the React SDK) If you have come to this post through a Google search, chances are that you are looking for this kind of info, and already know what all of this means, but if you didn’t, here is the quick and dirty of what I was trying to do (if you don’t care about this, click here to skip to the solution).
What I was trying to do:
Cloudinary is a cloud-based SaaS product that can serve as both a CDN and image manipulation platform. You most likely have used their services before without knowing it; thousands of sites use their platform as a backbone for fast image serving and manipulation. At the moment, my project involves the image processing part of Cloudinary. One feature that they offer is remote fetch – you give Cloudinary an already hosted image URL, and they mirror the image to your account and provide a CDN link. I wanted to go a step further and overlay a remote URL onto a Cloudinary image, but I ran into a bunch of issues.
Solution:
The crux of the solution is that you need to pass an object to the “overlay” method on a transformation object – like this:
let cloudinaryImageTag = cloudinaryInstance.imageTag(YOUR_IMAGE_BASE);
let tr = cloudinary.Transformation.new();
tr.overlay({
resourceType : 'fetch',
url : remoteSrc
});
cloudinaryImageTag.transformation(tr);
Failed Approaches / Lesson Learned:
The reason why I didn’t arrive at this solution faster is two-fold. One, Cloudinary, although powerful, has very fragmented documentation covering dozens of libraries and the distinction between them is not always clear. Two, and this was a learning lesson for me, I should have gone to check the actual source code of the libraries a lot sooner. I’ve always been a RTFM kind of guy, but sometimes even the manual doesn’t line up with what *actually* is going on. In this case, the documentation suggested a few conflicting things:
- It might not even be possible
- Pretty much every sample code snippet and documentation page referencing the overlay transformation parameter shows the way to do it is to pass in the publicId of the image you want to overlay like this:
-
let tr = cloudinary.Transformation.new(); tr.overlay(new cloudinary.Layer().publicId('my_cloudinary_publicId'));
- Of course, remote URLs don’t have a public ID – that is the whole point! I need Cloudinary to fetch the remote asset and clone it.
- This started to lead me to believe that what I wanted to do wasn’t even possible, and I would need to implement some logic to upload the remote image first and retrieve a publicId before trying to overlay…
- FALSE: After a bit of digging, I finally stumbled across this page, which suggested that maybe Cloudinary didn’t support this until recently (2017), but at least this was a sign I was still going in the right direction and they definitely do support it now
- That based on the above, I should base64 encode my remote URL and pass it as part of the publicId.
- The syntax for overlay is l_{publicId}, and since their example uses l_fetch:{base64_OF_IMAGE_URL}, I I tried this with some variation of code like this:
let tr = cloudinary.Transformation.new();
let publicId = 'fetch:' + base64.encode(remoteSrc);
tr.overlay(new cloudinary.Layer().publicId(publicId));
This did not work…
- Some of the (auto-generated?) documentation pages started pointing me towards FetchLayer, which extends the Layer class. This seemed like it made sense – FetchLayer is specifically a Layer that is supposed to be referencing a “fetched” asset, so it doesn’t have a PublicId
- This is where I started actually looking at the source code for the core Cloudinary JS library. If you look at the source, you can see that I was actually somewhat on the right path with my attempt #2, but me base64 encoding the URL was redundant since FetchLayer already does this.
- This is actually where I got really frustrated. Based on the source and docs, ANY of the following *should* have worked, but didn’t:
-
tr.overlay(new cloudinary.FetchLayer().url(remoteSrc));
-
tr.overlay(new cloudinary.FetchLayer(remoteSrc));
-
tr.overlay(new cloudinary.FetchLayer({ url : remoteSrc, resource_type : 'fetch' }));
-
- All of the above threw the same error:
Uncaught TypeError: layerOptions.substr is not a function
- FINALLY – I found two things, I think at the same time. Both brought me to the working solution
- There is an actual bug with FetchLayer – methods that rely on turning options into a string (.serialize(), .toHtml()) will all fail with the error outlined in attempt #3, because of this line of code, here, and pasted below:
-
} else if (/^fetch:.+/.test(layerOptions)) { result = new FetchLayer(layerOptions.substr(6)).toString(); }
- There is a pull request that specifically addresses and fixes this bug, which is partly how I found out about it in the first place, but it hasn’t been merged in yet
-
- If you pass a plain object into the transformation object constructor, the first if statement that checks for a plain object evaluates to true, and bypasses the faulty chunk of code entirely! This lets all the regular methods, such as .serialize(), work! The solution is up at the top of the page, or click here.
- There is an actual bug with FetchLayer – methods that rely on turning options into a string (.serialize(), .toHtml()) will all fail with the error outlined in attempt #3, because of this line of code, here, and pasted below: