Converting Image Url To Base64 In Ruby On Rails

Possible Gotcha and its solution

ยท

4 min read

Converting Image Url To Base64 In Ruby On Rails

THE PROBLEM THAT LED ME TO THIS ARTICLE

In one of my many adventures at work, I was working on a 3rd-party integration for KYC automation. The Kyc service provider we were integrating required that we send images (1 selfie photograph for govt ID verification and 8 greyscale images for liveness check) in their base64 representation as part of a payload. The frontend engineer and I initially agreed to hand over the base64 conversion to the frontend so I structured the GraphQL mutation for the backend service to collect a couple of base64 strings. Eg:

mutation{
    submitImages(
    selfieUrl: "outrageously-long-selfie-base64-string-from-frontend"
    livenessCheckUrls: [
        "outrageously-long-greyscale-image-as-base64-string-from-frontend-1",
"outrageously-long-greyscale-image-as-base64-string-from-frontend-2",
"outrageously-long-greyscale-image-as-base64-string-from-frontend-3",
"outrageously-long-greyscale-image-as-base64-string-from-frontend-4",
"outrageously-long-greyscale-image-as-base64-string-from-frontend-5",
"outrageously-long-greyscale-image-as-base64-string-from-frontend-6",
"outrageously-long-greyscale-image-as-base64-string-from-frontend-7",
"outrageously-long-greyscale-image-as-base64-string-from-frontend-8"
    ]
  )
}

Above, we have a submitImages mutation that sends a selfie and a list of images for liveness check that have all already been converted to base64 on the frontend. However, when the frontend engineer tried to implement this, they reported an issue where the base64 strings were too long to process as each one of them contained over 700,000 characters (oops!). We decided to ship this logic to the backend (that's me).

Backend Logic And A Gotcha

To do this on the backend, we decided to send the image URLs to the backend directly. So, the user takes in-app photos, the frontend writes a script to upload photos to a remote image warehouse (eg: Cloudinary), the warehouse returns a URL to the image resources and the frontend sends these URLs (instead of the base64 representations) to the backend.

mutation{
    submitImages(
    selfieUrl: "link-to-a-selfie"
    livenessCheckUrls: [
        "link-to-a-greyscale-image-1",
        "link-to-a-greyscale-image-2",
        "link-to-a-greyscale-image-3",
        "link-to-a-greyscale-image-4",
        "link-to-a-greyscale-image-5",
        "link-to-a-greyscale-image-6",
        "link-to-a-greyscale-image-7",
        "link-to-a-greyscale-image-8"
    ]
  )
}

On the backend, we can now write the logic/function to convert these images to base64. Since these images are not locally available on our machine/server, we need to extract the image resource via the provided URLs. We can use Ruby's in-built open-uri gem for this:

require 'open-uri'
img_url = "https://res.cloudinary.com/mobello/image/upload/v1650799554/trackstats/cache/2bc9255088a79d4ec8625252954ac7c2.jpg"
image = URI.open(img_url)
Base64.encode64(image.read).gsub("\n", '')

To explain the code above, we used open-uri to fetch an image resource from the internet (our image warehousing provider) through a given URL with image = URI.open(img_url), this will pull the resource to our local machine/server and store it in a temporary file/location. If you run this line in your rails console, you should get something like this: #<File:/var/folders/mb/blvjcg9s5cjd3gswyg75j8bc0000gn/T/open-uri20230422-4592-1e69h99> . Then further run the image.class and it should return Ruby's TempFile class.

After that line of code, we call the TempFile's read method to access our image resource which we now temporarily have on our machine and convert it to a base64 string representation. With this, we have our base64 string but it typically has a bunch of \n which represents the new line character. See below for an example of a base64 string with numerous new line (\n) characters:

To remove this, we add the gsub("\n", "") method chain to our base64 string to generally substitute any new line character with an empty character (""). And voila, that's it right?? Well, there's a gotcha ๐Ÿ‘‡๐Ÿพ

A NOT-SO-LITTLE GOTCHA

One problem I ran into with open-uri was that, while it fetches and returns the TempFile resource/path for the image URL appropriately, there are instances where it returns a StringIO object containing some metadata about the image resource instead of the expected TempFile storage of the resource. This issue typically happens because open-uri, by default is configured to return a StringIO object and will only create a TempFile containing the blob resource when the size of the StringIO data exceeds 10kb. This means that when the image size is too small, OpenURI will return StringIO (instead of a TempFile) containing the metadata of our resource during fetching. See below an example with a URL that points to an image resource measuring less than 10kb:

See that the returned data above is of class StringIO? OpenURL has an implicit constant called StringMax which sets the maximum data size to allow a TempFile as 10246.

Run this line OpenURI::Buffer::StringMax in your rails console and it should return the default max size:

To tackle this, we simply need to set the StringMax constant to 0, so that it now defaults to forcing a TempFile creation for any data we pass in like so:

OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
OpenURI::Buffer.const_set 'StringMax', 0

The above code first removes the StringMax constant from memory then redeclare it and assign it the value 0. If we run just this line OpenURI::Buffer.const_set 'StringMax', 0, we will get a warning message - warning: already initialized constant OpenURI::Buffer::StringMax

With this, our problem should be solved and we should no longer worry about our images returning as StringIO. We can put the above code in an initializer so it boots up with our application.

Thank you for reading.