ruby-on-railsrails-activestorageminimagick

Rails, Active Storage, MiniMagick - convert PDF pages to images (.png, .jpg, etc.) and upload to active storage


I am using rails, active storage and minimagick to try to convert a multi-page PDF into individual PNG images and then store the images with active storage.

I have been able to successfully load a PDF and convert the pages into png images using MiniMagick. However, I have only been able to get Minimagic to store the png images in the app/assets/images directory (as opposed to storing the png images as active storage attachments).

Does anyone know how get MiniMagick to store images with active storage?

Here is the code I'm using:

# Doc model with active storage attachments
class Doc < ApplicationRecord
  has_one_attached :pdf         #PDF file to upload
  has_many_attached :pdf_pages  #PNG image for each page of the PDF
end 

# PrepareDocs controller manages saving a PNG image of each PDF page. 
# NOTE: at this point, the PDF has already been attached to the @doc instance. 
# Now we are trying to create the PNG images of each page of the PDF
class PrepareDocsController < ApplicationController
  def show
    # Load @doc
    @doc = Doc.find(params[:id])
    # Get PDF from @doc
    mini_magic_pdf = MiniMagick::Image.open(ActiveStorage::Blob.service.send(:path_for, @doc.pdf.key))

    # Save first page of doc.pdf as a PNG (later I will update this to convert every page to a PNG, but I'm starting on page 1 for now)
    MiniMagick::Tool::Convert.new do |convert|
      # Prepare formatting for PNG image
      convert.background "white"
      convert.flatten
      convert.density 150
      convert.quality 100
      # Save PNG Image - This successfully creates a PNG of the first page, but I don't want to store it in my assets.
      convert << mini_magic_pdf.pages.first.path
      convert << "app/assets/images/page_1.png"   # This probably needs to be changed so that we are not storing in the app/assets/imdages directory
      # [??? insert additional code to attach the png image to @doc.pdf_pages using active storage instead of to the images directory?]
      # NOTE: if I try to access the newly created png in my views, I get "incorrect signature" OR "asset not available in asset pipeline" errors
    end

  end
end

Any help would be greatly appreciated!! Thanks!

UPDATE: I got it to work and here is the final code:

# Doc model with active storage attachments
class Doc < ApplicationRecord
  has_one_attached :pdf         #PDF file to upload
  has_many_attached :pdf_pages  #PNG image for each page of the PDF
end 

# PrepareDocs controller manages saving a PNG image of each PDF page. 
# NOTE: at this point, the PDF has already been attached to the @doc instance. 
# Now we are trying to create the PNG images of each page of the PDF
class PrepareDocsController < ApplicationController
  def show
    # Load @doc
    @doc = Doc.find(params[:id])
    # path to current pdf (ie @doc.upoad)
    pdf_path = ActiveStorage::Blob.service.path_for(@doc.pdf.key)
    # set minimagick image wrapper for pdf stored in @doc.uplaod
    magick = MiniMagick::Image.open(pdf_path)

    # run repeat block to save each page as an individual image
    magick.pages.each_with_index do |page, index|
      # set file name to "page_N"
      file_name = "page_#{(index+1).to_s}"
      # set path for tempfile that you are about to create (using rails post ruby 2.5 approach. Pre 2.5 ruby uses make_tmpname; post 2.5 ruby uses create; I like rails post 2.5 version)
      converted_file_path = File.join(Dir.tmpdir, "#{file_name}-#{Time.now.strftime("%Y%m%d")}-#{$$}-#{rand(0x100000000).to_s(36)}-.png")
      # create png and save to tempfile path
      MiniMagick::Tool::Convert.new do |convert|
        # prep format
        convert.background "white"
        convert.flatten
        convert.density 300
        convert.quality 100
        # add page to be converted
        convert << page.path
        # add path of page to be converted
        convert << converted_file_path
      end
      # attach using active storage - NOTE: this needs to be done AFTER the convert block
      @doc.pdf_pages.attach(io: File.open(converted_file_path), filename: file_name, content_type: "image/png")
      # remove tempfile
      FileUtils.rm(converted_file_path)
    end
  end 
end    

NOTE: I found 3 different methods for creating tempfiles (see here) depending on the ruby version and / or preference:

      # pre ruby 2.5 
      converted_file_path = File.join(Dir.tmpdir, Dir::Tmpname.make_tempname(["page_1", ".png"], nil))
      # ruby 2.5 and later
      converted_file_path = File.join(Dir.tmpdir, Dir::Tmpname.create(["page_1", ".png"]) {} )
      # rails version of pre ruby 2.5 
      converted_file_path = File.join(Dir.tmpdir, "#{file_name}-#{Time.now.strftime("%Y%m%d")}-#{$$}-#{rand(0x100000000).to_s(36)}-.png")

Solution

  • app/assets is a directory for your asset sources, images from there are copied to public/assets during asset compilation (usually done on deploy), so to be able to access created images - store them somewhere in public, like public/converted/page_1.png (it will have path /converted/page_1.png)

    MiniMagick is just a wrapper around ImageMagick command line utilities, so you need to save the result in a temporary location before uploading to activestorage and then use something like:

    converted_file_path = File.join(Dir.tmpdir, Dir::Tmpname.make_tmpname(["prefix-", ".png"], nil))
    ... # do convertation
    @doc.pdf_pages.attach(io: File.open(converted_file_path), filename: 'page1.png')
    FileUtils.rm(converted_file_path)