何気に「このPDF、数ページだけ画像にして保存したい…」という時があると思うのです。 …なかったとしても、万が一発生した時にGrimというgemが使えそうだったのでメモ。
処理の流れ
想定している処理は、
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ファイルに必要なフィールドを追加。
# db/migrate/201702140001_create_documents.rb class CreateDocument < ActiveRecord::Migration[5.0] def change create_table :douments do |t| t.string :name t.attachment :pdf t.timestamps end end end # db/migrate/201702140000_create_pdf_images.rb 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など、必要項目を足していきます。
# app/models/pdf_image.rb class PdfImage < ApplicationRecord has_attached_file :capture belongs_to :document validates_attachment_content_type :capture, :content_type => 'image/png' end # app/models/document.rb 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 # 切り出す枚数(デフォルト3ページ分) 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をコピーするメソッドを作ります。
# 画像切り出し用に、/tmp以下に配置されるPDFのtmpファイルをコピーしておく # 戻り値はコピーしたtmp fileのpath def copy_temp_pdf tempfile = document_params[:pdf]&.tempfile return unless tempfile pdf_dir = FileUtils.mkdir_p(Rails.root.join "tmp", "pdfs").first # PDFのtmpファイルをプロジェクトルートのtmp以下にコピー FileUtils.cp tempfile, pdf_dir "#{pdf_dir}/#{File.basename(tempfile)}" # File.join(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ファイルごにょごにょしてるあたり、もっと上手くやれたらなー。