Render Binary Image Data from AWS S3 in Template

I am on a bit of a journey here that I’m affectionately titling “From S3 to Visible Logo”. I am attempting to implement functionality to allow users to upload a logo for their organization, complete with a preview of the current logo (if it exists). I’m storing images in a non-public S3 bucket via Transmit, which returns a pre-signed URL that I then store in the database for later retrieval. I am using ExAWS.S3 to pull the image from an S3 bucket. On the wish list is resizing the image before storage, and I presently have Mogrify to hopefully pull that part off, but that’s likely to be a whole separate question.

What I’ve accomplished so far:

  • Using the stored URL to retrieve the image data from AWS S3
  • Pattern matching to separate out the image body, and headers (the status_code is just three numbers, so placing it in a variable seems a bit unnecessary)

What I would like to do is display this image in a template, either using an image_tag() or something similar (<img src="#{image_source}">). Thanks to this Stack Overflow answer, I have been able to get to the point at which I am presently. What I don’t quite get is how using the conn here results in a displayed image element.

Regarding the resizing, I’m presently using the client side to take the selected image, resize it to height/width limits using Canvas, then display a preview of the resized image. So far, my attempt to save that resized blob hasn’t gone as I’d hoped.

I realize there’s a lot going on here, so I’ll just focus on the strictly Elixir part: How do I go from pattern matching {:ok, %{body: <<[list_of_integers]>>, headers: [{"list of", "tuples"}], status_code: 200}} to rendering the resulting image_content in an <% image_tag(@image_content) %> or <img src="image_content_rendered.png">?

You can use base64 encoded data in img tag. javascript - How to display binary data as image - extjs 4 - Stack Overflow

in the template

# @image is the binary data of your image
<%= img_tag("data:image/jpeg;base64," <> Base.encode64(@image)) %>
1 Like

I think using Waffle/waffle_ecto will solve a lot of these problems for you. It will handle the image upload to s3, storing image path in db, coverting it to url for using as src on image tags and resizing (with imagemagick). waffle ecto part to have an ecto field mapped to your storage item (local or s3).

There’s a bit of context that would be helpful to make sure this is what you’re looking to do, e.g. use specifically s3 in this way, etc.

If you want you can base64 encode the image like @Ljzn mentioned

You could also point the img src in the template to a route in your server that indeed just fetches the data from S3 and serves it as a regular image with send_resp (this has the advantage that the image becomes cacheable by the browser when sent as an independent resource, but you would still be doing downloads whenever a new request comes in).

Is there an issue with using the presigned url as the src of the image instead of downloading it (like it has to be kept private, e.g. the bucket, or you can’t generate a smaller valid time window for the presigned url)?

You could also setup a cloudfront distribution to proxy the bucket bypassing having to share the bucket name if that was one of the concerns although signing the file and etc becomes more complex.

1 Like

Presently, the presigned URLs that are generated return errors (access errors if directly entered into the browser, or CORS error when used as an img src). I/we aren’t in charge of the setup of the bucket, so changing its permissions isn’t within our purview at the moment. I would love to just use that URL like one would ordinarily do, but clearly that’s asking too much of the universe.

After some thought, we’re likely to go in a slightly different direction, though I’m going to keep working on this as a side project so that, in the instance that I come to this bridge again, I’m not so inclined to incinerate said bridge.

Write your controller like this

def MyApp.ImageController do
  def show(conn, params) do
    with :ok <- validate_token(params),
          {:ok, content} <- generate_image(params) do
      conn  
      |> put_resp_content_type("image/png")
      |> send_resp(content)
    end
  end
end

The gist of this is that you want to generate a signed URL that results in a lazily generated image being pulled by the browser.

  • Use the built-in functions in Phoenix to generate and validate Tokens

  • Requests should come in as signed GET URLs and you can add cache-control headers

  • You can use the Briefly library to generate local temporary files / directories

  • You can put this behind a CDN

  • Do not use pre-signed S3 URLs to feed this service, instead use region/bucket/object tuples to identify the object and grab the object in the service that does thumbnail generation

3 Likes