Convert Base64 images to PDF with Prawn in Ruby
Tags: ruby, prawn
Published: 2019-05-20 12:57:14

A couple of weeks ago, I was working on a feature in [Camaloon](https://www.camaloon.com), which I ended up not using. However, I thought it would still be fun to share.

So, let's get to the issue without the boilerplate as it's late at night and I have to rise early :). 

> Problem: 
I was trying to implement a third-party API and hopefully get a Base64 encoded PDF file of different documents I was trying to fetch. Unfortunately, the third party API didn't provide the PDF files but instead, images.

So, I thought alright I will just use [Prawn](https://github.com/prawnpdf/prawn) and shove all images inside the Prawn pdf document and be done with it. Buttttt, images were in GIF format which I had to convert them to Prawn supported formats such as PNG/JPEG. 

So, seeing these issues, `Base64ImageToPdfConverter` was born. I wanted to keep the class generic in the case in the future we need to reuse it. 

> Solution: Accept an array of Base64 encoded images, check if they need a conversion to a PNG(Prawn supported format).Convert them to PNG in case they need conversion and include it in the prawn document, if not, include them directly.

Let's create our class.

```ruby
require 'prawn'
require 'tempfile'
require 'base64'

class Base64ImageToPdfConverter
  PRAWN_SUPPORTED_MIME_TYPES= ['image/png', 'image/jpeg'].freeze

  def initialize(base64_images:)
    @base64_images = base64_images
  end

  def run!
    return if base64_images.empty?

    open_tempfile(['combined-pdf', '.pdf']) do |pdf_file|
      base64_images.each do |base64_image|
        with_image_path(base64_image) do |image_file|
          # Process and Insert image to Prawn document
        end
      end
      # Render content to Prawn Document
      # Encode PDF in base64
    end
  end
  
  private
  attr_reader :base64_images

  def open_tempfile(*args)
    f = Tempfile.new(*args)
     yield(f)
    ensure
     f.close!
  end

  def with_image_path(base64_image)
    open_tempfile('image') do |f|
      f.write Base64.decode64(base64_image)
      f.flush
      yield f
    end
  end
end
```

So what the hell was that? Here, we are creating one temp file for final PDF that we will combine all images in and a temp file for each image, decoding Base64 and writing to it as we will need it to work with ImageMagick conversion soon. 

Let's add our conversion logic.

```ruby
require 'prawn'
require 'tempfile'
require 'base64'
class Base64ImageToPdfConverter
  PRAWN_SUPPORTED_MIME_TYPES= ['image/png', 'image/jpeg'].freeze

  def initialize(base64_images:)
    @base64_images = base64_images
  end

  def base64_pdf
    return if base64_images.empty?

    open_tempfile(['combined-pdf', '.pdf']) do |pdf_file|
      base64_images.each do |base64_image|
        with_image_path(base64_image) do |image_file|
          insert_image(image(image_file.path))
        end
      end
      # render and return the Base64 encoded PDF.
      prawn_document.render_file pdf_file.path
      base64_encoded(pdf_file.path)
    end
  end

  private

  attr_reader :base64_images
  attr_accessor :prawn_document

  def insert_image(string_io)
    prawn_document.start_new_page
    prawn_document.image string_io, position: :center
  end

  def image(file_path)
    if need_conversion?(file_path)
      # We are using imagemagick convert command to convert to png
      system "convert #{file_path} #{file_path}.png"

      # As we do not need a Tempfile but an object that responds to
      # read and write to use in Prawn document. We can use StringIO and
      # unlink tempfile.

      string_io= StringIO.new(File.read("#{file_path}.png"))
      File.unlink("#{file_path}.png")
    else
      string_io= StringIO.new(File.read(file_path))
    end

    string_io
  end

  def need_conversion?(file_path)
    !PRAWN_SUPPORTED_MIME_TYPES.include?(mime_type(file_path))
  end

  def mime_type(file_path)
    `file #{file_path} --mime-type -b`.strip
  end

  def prawn_document
    @prawn_document ||= Prawn::Document.new margin: 0,
                                            skip_page_creation: true
  end

  def base64_encoded(file_path)
    Base64.encode64(File.read(file_path))
  end

  def open_tempfile(*args)
    f = Tempfile.new(*args)
    yield(f)
  ensure
    f.close!
  end

  def with_image_path(base64_image)
    open_tempfile('image') do |f|
      f.write Base64.decode64(base64_image)
      f.flush
      yield f
    end
  end
end

base64_gifs = ['R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=']

converter = Base64ImageToPdfConverter.new(base64_images: base64_gifs)
puts converter.base64_pdf
```

So here we go, we check for each image's Mime Type, convert them using ImageMagick's `convert` command to `PNG` and include it in Prawn document. We return the Base64 encoded PDF.

You can write the result to a file if you would like to create one:

```ruby
base64_gifs = ['R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=']
File.open "your_pdf_document.pdf", "wb" do |file|
  file.write Base64.decode64(Base64ImageToPdfConverter.new(base64_images: base64_gifs)
                                                      .base64_pdf)
  file.close
end
```

Credits to [Roger](https://github.com/rogercampos/) for the review of the PR and shaping the class to be a bit better than its first iteration :D