何気に「このPDF、数ページだけ画像にして保存したい…」という時があると思うのです。
…なかったとしても、万が一発生した時にGrimというgemが使えそうだったのでメモ。
github.com
処理の流れ
想定している処理は、
1. PDFを保存&S3にアップロード
2. 保存したPDFから3枚画像に切り出す
3. 切り出した画像を保存&S3にアップロード
です。では早速。
モデル準備
↑のイメージでモデルを作ります。
DocumentはPDFを持つように、PdfImageはcapture(=PDFから切り出した画像)を持つようにします。
Documentは複数枚の画像を持つので、has_manyで。
PDFと切り出した画像のアップロードはpaperclipを使います。
今回はpaperclipは入っている前提で進めます。
必要なファイルを生成
$ rails g model PdfImage
$ rails g scaffold documents
migrationファイルに必要なフィールドを追加。
class CreateDocument < ActiveRecord::Migration[5.0]
def change
create_table :douments do |t|
t.string :name
t.attachment :pdf
t.timestamps
end
end
end
class CreatePdfImage < ActiveRecord::Migration[5.0]
def change
create_table :pdf_images do |t|
t.attachment :capture
t.references :document, foreign_key: true
t.timestamps
end
end
end
migrationファイルができたら、コマンド実行。
$ RAILS_ENV=development rails db:migrate
モデルにattachmentのvalidationなど、必要項目を足していきます。
class PdfImage < ApplicationRecord
has_attached_file :capture
belongs_to :document
validates_attachment_content_type :capture, :content_type => 'image/png'
end
class Document < ApplicationRecord
has_attached_file :pdf
has_many :pdf_images, dependent: :destroy
validates_attachment_content_type :pdf, :content_type => 'application/pdf'
end
PDFの場合、白背景が透過されてしまい、白い背景が真っ黒になる場合もあるので、その時はhas_attached_file
のオプションにImageMagickでの変換指定することで回避できます。
has_attached_file :pdf, convert_options: {all: '-flatten'}
Grim処理のworkerを作成
PDFから画像を切り出して保存&S3でアップロードまでやろうとすると時間がかかるので、Workerで処理するようにします。
class CreatePdfImagesWorker
include Sidekiq::Worker
sidekiq_options queue: :create_pdf_images, retry: false
def perform(document_id, target_file)
document = Document.find(document_id)
old_pdf_images = document.pdf_images.presence
pdf = Grim.reap(target_file)
path = FileUtils.mkdir_p(Rails.root.join "tmp", "pdfs", document_id.to_s).first
page_num = 3
page_num = pdf.count if pdf.count < 3
page_num.times do |page|
pdf_file_name = document.pdf_file_name.split(".")[0]
pdf_image_path = "#{path}/#{pdf_file_name}_#{page}.png"
if pdf[page].save(pdf_image_path)
file = File.open(pdf_image_path)
PdfImage.create(image: file, document_id: document_id)
file.close
end
end
old_pdf_images.each{|pdf_image| pdf_image.delete} if old_pdf_images
FileUtils.remove_entry_secure path
FileUtils.remove_entry_secure target_file
end
end
controllerでworker呼び出し
controller内でやることは2つです。
1. PDFのtmpファイルを自分の扱いやすいところにコピーする
2. workerを呼び出す
1. PDFのtmpファイルを自分の扱いやすいところにコピーする
privateメソッドとして/tmp以下にあるPDFをコピーするメソッドを作ります。
def copy_temp_pdf
tempfile = document_params[:pdf]&.tempfile
return unless tempfile
pdf_dir = FileUtils.mkdir_p(Rails.root.join "tmp", "pdfs").first
FileUtils.cp tempfile, pdf_dir
"#{pdf_dir}/#{File.basename(tempfile)}"
end
document = Document.new
等で生成したインスタンスをsaveすると、PDFをアップロードする際にtmpファイルも消えてしまうので、saveする前にtmpファイルをコピーする copy_temp_pdf
メソッドを呼び出します。
コピーしたファイルのパスが返ってくるので、変数に入れておきます。
2. workerを呼び出す
documentのidが必要になるので、workerの呼び出しはインスタンスsave後に行います。
class DocumentsController < ApplicationController
def create
@document = Document.new(document_params)
target_file = copy_temp_pdf
if @document.save
CreatePdfImagesWorker.perform_async(@document.id, target_file) if target_file
format.html
end
end
end
ざっとこんな感じでした。
ゴリゴリ気合いでやるか、複数枚アップロードするしかないのかなと思っていた矢先にこのGem。
やることが少ない分、使いやすい&わかりやすかったです。
tempファイルごにょごにょしてるあたり、もっと上手くやれたらなー。