DIVX テックブログ

catch-img

RailsのPDFライブラリをReactを活用して出力する(PrawnによるPDF出力の実装ガイド)


目次[非表示]

  1. 1.はじめに
  2. 2.Prawnとは
  3. 3.事前準備
  4. 4.ReactでPDFを出力する上での問題点
  5. 5.Prawnを別言語上でPDF出力する方法
  6. 6.まとめ
  7. 7.お悩みご相談ください
  8. 8.参考資料

はじめに


DIVXでエンジニアをしている松野と申します。


今回の記事では、Railsライブラリの一つであるPrawnを用いて、PDFを異なる言語を介して出力する方法について説明したいと思います。
このテーマを選んだ理由としては、過去の案件で2言語を使用したPDF出力機能の実装に非常に苦労した経験があるためです。


ただ解説するだけでは理解が難しいと思うため、実際にフロントエンドをReact、バックエンドをRailsのAPIを使用した簡単なページを例にPrawnで描写したPDFファイルをフロントから実際に出力させてみたいと思います。


Prawnとは

https://prawnpdf.org/

そもそもPrawnというものが何かわからない!という方ももちろんいらっしゃると思います。
PrawnはRuby用のPDF生成ライブラリで、Railsだけでなく、Rubyのアプリケーションから直接PDFドキュメントを作成できます。


RailsにはPDFkitやWicked PDFといったPDF生成ライブラリがありますが、これらはHTML画面をPDFに変換して出力します。一方、PrawnはRubyのコードを使用して直接PDFを描画し出力するため、Rubyに慣れている方であれば、複雑なPDFを簡単に作成できるメリットがあります。


事前準備

まずはReactのアプリを作成し、PDFを出力するボタンとボタンを押下した際に発生するイベントを作成します。

pdf.tsx

import React from 'react';
// プロパティの型を定義
interface PdfProps {
  pdfIssuanceClick: () => void;
}

const Pdf: React.FC<PdfProps> = ({ pdfIssuanceClick }) => {
  return (
    <div>
      <button
        className="c-button__item"
        onClick={pdfIssuanceClick}
      >
        PDFファイルを出力する
      </button>
    </div>
  );
};

export default Pdf;

pdfContainer.tsx

import React from 'react';
import Pdf from './pdf';

const PdfContainer: React.FC = () => {
  // PDFを出力するボタンのクリックハンドラー
  const pdfIssuanceClickHandler = async () => {
    // PDF出力処理をここに実装
    const result = await API名();
  };

  return (
    <div>
      <Pdf pdfIssuanceClick={pdfIssuanceClickHandler} />
    </div>
  );
};

export default PdfContainer;

次に、Gemfileにprawnを記述し、Gemをインストールします。

gem 'prawn'
gem 'prawn-table'

prawnがインストールされたことを確認したら、まずはコントローラ部分を記述していきます。
pdf_controller.rb

class Api::V1::PdfController < Api::V1::ApplicationController
	def pdf_issuance
		#PDF形式で出力する
		respond_to do |format|
			format.pdf do
				send_data pdf,
				filename: "PDFファイル名",
				type: 'application/pdf',
				disposition: 'attachment'
			end
		end
	end
end

次にPrawn::Documentを継承したクラスを作成します。
pdf.rb

class Pdf < Prawn::Document
  def initialize
    super(page_size: 'A4') #A4サイズのPDFを新規作成
    stroke_axis # 座標を表示(これがあると便利)

    # 日本語フォントの読み込み
    font_families.update('JP' => {
      normal: 'app/assets/fonts/ipaexm.ttf',
      bold: 'app/assets/fonts/ipaexg.ttf'
    })
    font 'JP', style: :bold
  end
end

これらのファイルを作成し、ReactとRailsを経由するAPIを作成することで準備完了です。


ReactでPDFを出力する上での問題点

事前準備を行っただけではPrawnで描写したPDFファイルがダウンロードされるわけではありません。このままReactで記述したPDF出力ボタンを押下すると、APIを経由してPrawnで描写したPDFファイルは返却されますが、この場合、問題が2点発生します。


1点目は、PDFファイルが正しく生成されている場合、APIからPDFデータが返却されますが、Reactでそのデータをダウンロードするための処理が必要な点です。

respond_to do |format|
	format.pdf do
		send_data pdf,
		filename: "PDFファイル名",
		type: 'application/pdf',
		disposition: 'attachment'
	end
end

まず、PDFファイルを出力するには、Railsのコントローラーでpdf_issuanceアクションを実行する必要があります。このアクションでは、RailsからのレスポンスとしてPDFデータを用意し、Content-Dispositionヘッダーに「attachment」を設定することで、ブラウザがファイルを直接表示するのではなく、ユーザーにダウンロードさせるようにしてからPDFデータを返すようにしています。


しかし、ReactではaxiosというHTTP通信を行うライブラリを使用しており、axiosからAPIにアクセスしてPDFデータを取得するため、axiosのヘッダにはContent-Dispositionの情報はなく、React側でPDFデータを処理する際にその情報が利用できないため、データだけ返却される状態になってしまいます。


つまりRailsで設定したContent-DispositionヘッダーはReactへのレスポンスには反映されていないため、PDFデータは返却されますが、React側での適切な処理がないと、ユーザーがダウンロードできる形で表示されません。
したがって、ReactでPDFデータを出力する処理を記述する必要があります。


2点目は、ブラウザによってPDFの表示方法が異なる場合があり、特に<a>タグのdownload属性がすべてのブラウザでサポートされているわけではない点です。
この問題の原因は、PDFデータのリンク作成時に指定する<a>タグのダウンロード属性がブラウザによってサポートされていないことにあります。


ReactでPDFを出力・ダウンロードする際には、PDFデータをBlobとして生成し、それをURL.createObjectURLを用いてBlob URLを作成し、PDFダウンロード用のリンクを作成する必要があります。この際にHTMLのダウンロード属性を追加しますが、この属性は主要なモダンブラウザでサポートされていますが、特定のバージョンや設定によっては異なる動作をすることがあります。スマートフォン版の一部のブラウザでは動作が異なる場合がありますが、主要なブラウザではダウンロード属性はサポートされています。


一部のブラウザではダウンロード属性の挙動が異なる可能性があります。Safariでは、PDFファイルが新しいタブで表示されることが一般的ですが、ユーザーの設定やバージョンによって異なる場合があります。出力方法を統一するためには、各ブラウザの挙動を考慮した実装が必要ですが、ダウンロード属性自体の利用が非推奨であるわけではありません。


Prawnを別言語上でPDF出力する方法

以上の注意点を踏まえてReactでPDFを出力してみようと思います。
前述の通り、prawnで描写したPDFファイルを別言語上で出力する場合、返却されたPDFデータをblobオブジェクトにする必要があります。
APIから返却されたデータはpdfContainer.tsxにおけるresult.dataに格納されているため、こちらをblobオブジェクトにしたあと、PDFダウンロード用のリンクを作成します。

pdfContainer.tsx

import React from 'react';
import Pdf from './pdf';

const PdfContainer: React.FC = () => {
  // PDFを出力するボタンのクリックハンドラー
  const pdfIssuanceClickHandler = async () => {
    // PDF出力処理をここに実装
    const result = await API名
    // データをblobオブジェクトに変換、URLリンクを作成
    const url = window.URL.createObjectURL(new Blob([result.data], {type: 'application/pdf'}))
		const link = document.createElement('a')
		link.href = url
		const fileName = 'PDFファイル'
		link.setAttribute('download', fileName)
		document.body.appendChild(link)
		link.click()
  };

  return (
    <div>
      <Pdf pdfIssuanceClick={pdfIssuanceClickHandler} />
    </div>
  );
};

export default PdfContainer;

次にそれぞれのブラウザごとに出力方法を条件分岐するようにします。
今回はReactライブラリであるreact-device-detectを利用して判別していきたいと思います。
react-device-detectは、ユーザーエージェントを検出し、特定のデバイスやブラウザに基づいて条件分岐を行うためのライブラリです。Webサイトが、ユーザーエージェントによって異なる内容を表示したり、 特定のユーザーエージェントにのみ内容を表示する特徴を活かして、ブラウザのユーザーエージェント文字列を解析・特定を行ってくれます。


ではまず、react-device-detectをインストールします。

npm install react-device-detect --save

インストール後、Reactのコンテナ部分の処理にreact-device-detectをインポートします。
今回はSafari、Chrome、Edgeごとに処理を分けるようにします。

import React from 'react';
import Pdf from './pdf';
// react-device-detectをインストールする
import { isSafari, isChrome, isEdge } from 'react-device-detect'

const PdfContainer: React.FC = () => {
  // PDFを出力するボタンのクリックハンドラー
  const pdfIssuanceClickHandler = () => {
    // PDF出力処理をここに実装
    const result = await API名();
    // データをblobオブジェクトに変換、URLリンクを作成
    const url = window.URL.createObjectURL(new Blob([result.data], {type: 'application/pdf'}))
		const link = document.createElement('a')
		link.href = url
		const fileName = 'PDFファイル'
		link.setAttribute('download', fileName)
		document.body.appendChild(link)
		link.click()
  };

  return (
    <div>
      <Pdf pdfIssuanceClick={pdfIssuanceClickHandler} />
    </div>
  );
};

export default PdfContainer;

この記述でリンクURLを作成することでダウンロード属性がサポートされているChromeではPDFファイルをダウンロードすることができます。


SafariやEdgeでは、Blobで作成したURLを開くことでPDFを表示できますが、ダウンロードする際にはブラウザの設定によって異なる挙動を示すことがあります。必要に応じて、ユーザーに手動で保存を促すことができます。


const PdfContainer: React.FC = () => {
  // PDFを出力するボタンのクリックハンドラー
  const pdfIssuanceClickHandler = () => {
    // PDF出力処理をここに実装
     const result = await API名();
    // データをblobオブジェクトに変換、URLリンクを作成
    if (isChrome) {
			const url = window.URL.createObjectURL(new Blob([result.data], {type: 'application/pdf'}))
			const link = document.createElement('a')
			link.href = url
			const fileName = 'PDFファイル'
			link.setAttribute('download', fileName)
			document.body.appendChild(link)
			link.click()
		} else if (isSafari || isEdge) {
			const url = window.URL.createObjectURL(new Blob([result.data], {type: 'application/pdf'}))
			window.open(url)
		}
  };

この実装で、PDFデータを各ブラウザごとに適切に処理することが可能になりますが、ブラウザによってはユーザーの操作が必要な場合があります。


まとめ

今回はRailsのPDFライブラリであるPrawnを使用して、ReactアプリケーションからPDFを出力する方法を記事にしてみました。
Prawnは別言語で出力すると一手間かかってしまうものの、各ブラウザの挙動とRailsを知っていれば簡単にPDFファイルを描写して出力することができるため、
少しでも皆様のためになる記事になっていたら幸いです。最後まで御覧いただきありがとうございました。


お悩みご相談ください


  ご相談フォーム | 株式会社divx(ディブエックス) DIVXのご相談フォームページです。 株式会社divx(ディブエックス)



参考資料

【Rails】PDF出力機能を実装できるGem「Prawn」の使い方と実例(出力サンプル)をご紹介

  【Rails】PDF出力機能を実装できるGem「Prawn」の使い方と実例(出力サンプル)をご紹介 – こばっちプログ(PLOG) https://plog.kobacchi.com/rails-pdf-output-gem-prawn/



お気軽にご相談ください


ご不明な点はお気軽に
お問い合わせください

サービス資料や
お役立ち資料はこちら

DIVXブログ

テックブログ タグ一覧

人気記事ランキング

GoTopイメージ