Uploading Camera Image Using Canvas And Rails
Published: 2023-05-23 10:04:47

## Introduction

Hello! 😃

In this tutorial I will show you how to take a screenshot using the MediaDevices API, show the screenshot on the screen and how to upload the taken image to a rails API application to be saved.

---

## Requirements
- Basic Knowledge of Rails
- Basic Knowledge of APIs

---

## Setting Up The Rails Application

First we need to set up the backend to be able to receive and save the image, since we will just be accepting and saving an image file we don't really need a full blown rails application. To create a simple rails API only application run the following command:

```bash
rails new image-upload --api
```

The api flag tells Rails to set up the application as an API-only application, which means it excludes the middleware/views etc.

Next move inside the created directory and initialize active storage via the following command:

```bash
rails active_storage:install
```

The above will initialize active storage which is used to handle file uploads and save them.
It also generates a database migration file which is used to manage file uploads, run the following command to migrate it to the database:

```bash
rails db:migrate
```

Next we need to create a model for the image, this can be done via the following command:

```
rails g model Image
```

The above will create a new application record which also needs to be inserted into the database, so again we run the following:

```bash
rails db:migrate
```

Now that we have a model for our image, we need to edit it to be able to add an attachment to it. Open up app/models/image.rb and change it to the following:

```ruby
class Image < ApplicationRecord
  has_one_attached :image
end
```

The "has_one_attached" line tells Rails that each Image can have one attached file, in this case "image". Now that we have set up the application we can start working on the Controller. 🤓

---

## Creating The Controller

Next we need to create the controller to handle the image upload. 
This can be done via the following command:

```bash
rails g controller Api::Images
```

Open the newly created controller file and add a method called "save_image" to it as follows:

```ruby
class Api::ImagesController < ApplicationController
  def save_image
    begin
      data = params[:image].sub("data:image/png;base64", "") 
      image_data = Base64.decode64(data)
      image_io = StringIO.new(image_data)
      image_io.class.class_eval { attr_accessor :original_filename, :content_type }
      image_io.original_filename = "image.png"
      image_io.content_type = "image/png"

      @image = Image.new
      @image.image.attach(io: image_io, filename: "image.png", content_type: "image/png")

      if @image.save
        render json: { message: "Image saved successfully" }, status: :ok 
      else
        render json: { error: @image.errors.full_message }, status: :unprocessable_entity
      end 
    rescue => e
      render json: { error: "An error occurred whilst saving the image" }, status: :internal_server_error
    end 
  end 
end
```

What the above method does is get the "image" parameter from the incoming request, which is expected to be base64-encoded PNG image. We remove the "data:image/png;base64," prefix as this is not needed for decoding.

Next we need to create StringIO object from the image data. This is necessary because the method we'll use to attach the image to an Active Storage record expects an IO like object. 

We then add the "original_filename" and "content_type" accessors to the StringIO object and then declare them.

Finally we create a new image object and attach the image file to the newly created object and then attempt to save it. 

If all goes well the image will be saved to local storage, if not an error will be returned. 

Hopefully my explanation was understandable. 😅 Now that we have created the controller, next we need to set up the routes.

---

## Creating The Routes

Next we need to set up our routes to actually be able to handle requests. 

Open up the "config/routes.rb" file and add the following lines:

```ruby
namespace :api do
  post "/save_image", to: "images#save_image"
end 
```

The above sets up a route that responds to POST requests at the "api/save_image" path, and directs those requests to the "save_image" action in the controller we created. 😁

Finally we need to set up CORS.

---

## Setting Up CORS

Because we will be making requests from a HTML page served on a different server, we will need to configure CORS.

First uncomment the following line in your Gemfile:

```
gem "rack-cors"
```

Then install the above gem with the following command:

```bash
bundle install
```

Next we need to edit the cors config file, open up "config/initializers/cors.rb" file and add the following:

```ruby
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  if Rails.env.development?
    allow do
      origins "*" 
      resource "*",
        headers: :any,
        methods: [:get, :post]
    end 
  end 
end
```

The above permits external requests from anywhere and allows get and post methods. We also only allow this if the environment is development. 

That's it for the rails API side of things, now we can move on to the frontend code. 😎

---

## Creating The Frontend Code

Finally we will code the frontend to be able to access the user's camera, take a screenshot and upload it to the server. The front end code is a lot simplier than the above. 
Open up a file anywhere called "index.html" and add the following HTML:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Screenshot Uploader</title>
    <meta charset="UTF-8"/>
  </head>

  <body>
    <video id="localVideo" width="640" height="480" autoplay></video>
    <button id="capture" onclick="captureImage()">Take Picture</button>
    <canvas id="localCanvas" width="640" height="480"></canvas>

    <script>
      const localVideo = document.getElementById('localVideo');
      const localCanvas = document.getElementById('localCanvas');
      const context = localCanvas.getContext('2d');

      navigator.mediaDevices.getUserMedia({ video: true, audio: false })
        .then((stream) => {
          localVideo.srcObject = stream;
          localVideo.load();
        })
        .catch((error) => console.error(error));

      const captureImage = () => {
        context.drawImage(localVideo, 0, 0, 640, 480);

        const data = localCanvas.toDataURL('image/png');

        fetch('http://localhost:3000/api/save_image', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
          },
          body: JSON.stringify({ image: data })
        });
      };
    </script>
  </body>
</html>
```

What the above code does is access the user's camera on page load, if granted the local video will start playing the user's camera footage. If you click on the "Take Picture" button the canvas object will show the screenshot, the screenshot will also be uploaded to the rails API and stored on disk. 

Now to actually try out the application. 😃

---

## Running The Application

First start the Rails server with the following command:

```bash
rails s
```

Next we need a server for the HTML page, I just used a simple Python one-liner but feel free to use Nginx etc.

```bash
python -m http.server 8000
```

Now that everything is running, you can now access the HTML page via the following URL:

http://localhost:8080

Permit access to your camera and try taking a picture.  📷

The canvas should contain the screenshot and the screenshot should also be uploaded to the Rails application located somewhere under "storage". 😆

---

## Conclusion

In this tutorial I have shown how to take a screen shot using the Media Devices API and shown how you can upload and save that screenshot to a Rails API backend.

Please let me know if you have any questions or improvements etc. 
As always you can find the sample code on my Github repo:
https://github.com/ethand91/screenshot-upload

I hope this tutorial has helped you somewhat, and as always happy coding! 😎

---

Like me work? I post about a variety of topics, if you would like to see more please like and follow me.
Also I love coffee. 

[![“Buy Me A Coffee”](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/ethand9999)

If you are looking to learn Algorithm Patterns to ace the coding interview I recommend the [following course](https://algolab.so/p/algorithms-and-data-structure-video-course?affcode=1413380_bzrepgch)