蓝天,小湖,湖水中一方小筑

Save base64 image with carrierwave and bootstrap-wysiwyg

tl;dr

The pasted image will be converted to base64 encoded format, which will hit response size limitation of server. This article is talking about save image to file with carrierwave.

The source code is available at github repo

Next is the full version.

The problem

Recently I have faced a task to upload a image by a rich format text editor. The web server is Rails, so I selected carrierwave as the upload gem, and [bootstrap-wysiwyg](http://mindmup.github.io/bootstrap- wysiwyg/) as rich format text editor.

bootstrap-wysiwyg supports inserting image into edit area, and uploaded image via base64. Every thing is okay in development enviromnent, but I have met problem while deploying to production server.

The root cause of the problem is response size exceed the max limitation. The uploaded base64 encoded image are saved as string, and will be returned in response body. I have tried increasing response body size limitation but take no effect, so I switched to method that saving image to file.

Solution

This section only shows how to get image and save via carrierwave, please refer to the source of the other contents.

The sample project is a simple post manage system, each post contains title and content field, and the content field is rich format text.

The passed in base64 encoded image is started with this string:

data:image/jpeg;base64,

Then following the image data.

The image uploaded is surrounded by <img> tag, so I added a pre processing to the content uploaded. The logic is simple: save found <img> tag to a file with carrierwave, and replace the base64 data to file path. The primary code is here:

def process_base64_content content
  return if content.nil?
  return content if not content.match /</
  rslt = ''
  content.split("<").each do |elem_str|
    if elem_str[0..2] == "img"
      if elem_str.match(%r{data:(.*?);(.*?),(.*?)">$})
        img_data = {
          :type =>      $1, # "image/png"
          :encoder =>   $2, # "base64"
          :data_str =>  $3, # data string
          :extension => $1.split('/')[1] # "png"
        }

        other_img = PostImage.new
        img_data_str = img_data[:data_str]
        img_data_sio = CarrierStringIO.new(Base64.decode64(img_data_str))
        other_img.image = img_data_sio
        other_img.save
        rslt += view_context.image_tag(other_img.image.url)
      else
        rslt += "<#{elem_str}" if not elem_str.empty?
      end
    else
      rslt += "<#{elem_str}" if not elem_str.empty?
    end
  end

  rslt
end

PostImage is a model used for saving image. CarrierStringIO is also a user defined class to provide functions original_filename and content_type, which are required by carrierwave. Here is the definition of this class:

class CarrierStringIO < StringIO
  def original_filename
    # the real name does not matter
    "image.jpeg"
  end

  def content_type
    # this should reflect real content type, but for this example it's ok
    "image/jpeg"
  end
end

class PostImage
  include Mongoid::Document
  include Mongoid::Timestamps

  def image_data=(data)
    sio = CarrierStringIO.new(Base64.decode64(data))
    self.image = sio
  end

  mount_uploader :image, PostImageUploader
end

The last thing is the carrierwave uploader: PostImageUploader. This is a simple uploader that only save the image to file.

# encoding: utf-8

class PostImageUploader < CarrierWave::Uploader::Base

  storage :file

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

end

In the sample project, the file type and file name are hard coded in CarrierStringIO, please feel free to modify code as needed.

Note: There is an bug of the code: The image can’t be extracted out if inserted into a text paragraph. I will fix this once I have time.