DEV Community

Sahil Gadimbayli
Sahil Gadimbayli

Posted on • Edited on

Convert Base64 images to PDF with Prawn in Ruby

A couple of weeks ago, I was working on a feature in Camaloon, 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 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.

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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

Credits to Roger for the review of the PR and shaping the class to be a bit better than its first iteration :D

Top comments (0)