Laravel-dompdfを使って困ったあれこれ

laravel-dompdf-troubledLaravel

Laravelを使っていてPDF出力をしたいときに候補として上がってくるのが「laravel-dompdf」。

今回はlaravel-dompdfを使ってPDF出力したときに結構困った点があったのでそのあたりをまとめようと思います。

今回やりたかった内容

・動的に出力したHTMLをPDF化
・多少の装飾(テーブルのカラーなど)
・日本語表示
・複数ページ

それほど難しいことはしようとしておらず、シンプルにHTMLの内容をPDFで出力したいということでした。

しかし、これらを実現するためにいくつか罠がありました。

laravel-dompdfの導入手順

ただPDFを出力するだけであればlaravel-dompdfは簡単だと思います。

まずはlaravel-dompdfをインストール

composer require barryvdh/laravel-dompdf

次に出力するPDFの元となるviewファイルの作成

<!doctype html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
    <h1>Hello World</h1>
</body>
</html>

最後に出力するコントローラー

class PdfController extends Controller
{
    public function downloadPdf() {
        $pdf = \PDF::loadView('hello');
        return $pdf->stream('hello.pdf');
    }
}

loadViewメソッドを使用することでLaravelのviewメソッドと同じように、第一引数にviewファイル名、第二引数にデータを渡すことができます。

ただPDFを出力するだけであればこれくらいでできてしまいます。

もう少し詳しい使い方についてはこちらを参照してください。

GitHub - barryvdh/laravel-dompdf: A DOMPDF Wrapper for Laravel
A DOMPDF Wrapper for Laravel. Contribute to barryvdh/laravel-dompdf development by creating an account on GitHub.

laravel-dompdfを使って困ったこと

今回PDF出力する上で対応しなければいけないことや困ったことは以下になります。

  • 日本語対応(Noto Sans JP)
  • 改ページ
  • ページ番号表示
  • CSSが不完全
  • 改行されない
  • PDFの高さ超えると文章が見切れる

一つずつ対応方法をまとめていきます。

日本語対応

日本のサービスであれば基本的に日本語でのPDF表示をしたいと思います。

しかしlaravel-dompdfではデフォルトでは日本語には対応しておらず、文字化けしてしまうため多少の設定が必要になります。

私が見た記事の多くはIPAフォントを使う例が紹介されていましたが、今回はNoto Sans JPというフォントを使う必要があったためあまり参考になる記事がありませんでした。

というのもGoogle Fontsからダウンロードできるフォントファイルは.otfという拡張子のファイルで、laravel-dompdfに対応しているのは.ttfという拡張子のフォントファイルでダウンロードしたものをそのまま使うことができませんでした。

2023年8月時点ではGoogleフォントからNotoSansJPのフォントすると.ttfのファイルになっていたためそのまま使用できました。

そのためフォントの形式を変換する必要があります。

私が対応した方法としてはOTFからWOFFに変換、WOFFからTTFに変換という方法です。

WOFFコンバータというソフトを使って対応したのですが、OTFからTTFには一発で変換できず、一度WOFFに変換してから対応することで実現できました。

フォントの形式の違いについてはこちらをご参照ください。

ここまでが日本語対応するための事前準備なのですが、あとはこのフォントファイルをCSSの@font-faceで指定してあげるだけです。

@font-face{
    font-family: Noto Sans JP;
    font-style: normal;
    font-weight: normal;
    src:url('{{ storage_path('fonts/NotoSansJP-Regular.ttf')}}');
}
@font-face{
    font-family: Noto Sans JP;
    font-style: normal;
    font-weight: bold;
    src:url('{{ storage_path('fonts/NotoSansJP-Bold.ttf')}}');
}
body {
    font-family: Noto Sans JP, sans-serif;
}

ここで注意しないといけないのはBoldのフォントも指定しないとh1タグなど元々太文字の文字が文字化けしてしまいます。

これを設定した上でPDFを出力するとfontsディレクトリ配下にinstalled-fonts.jsonなどいくつかのファイルが作成されます。

実際にはこの作成されたファイルを読み取っているようです。

フォントファイルのサイズが大きい

Google Fontsからダウンロードしたフォントファイルは5MBほどあり結構重たいのでサブセット化してあげます。

サブセット化とは要は軽量化です。

サブセット化についてはサブセットフォントメーカーを使って必要な文字だけを指定して軽くしてあげます。

指定する文字が足りないとPDF出力した際に表示されないといったことがおきます。

私は実際になどの環境依存文字が表示されないといったことに遭遇しました。

サブセット化すると5MBあったファイルが730KBまで軽くなりました。

詳しくは以下をご参照ください。

サブセット化で日本語Webフォントの容量を軽量化する方法
Webサイトをデザインする際、フォント選びは&#12...

改ページ

PDFをブロックごとにページを分けたいといった場合はCSSのpage-break-afterを使用することで実現できます。

改ページさせたいタグに対してpage-break-after: always;と指定してあげることでそのタグから下の内容が次ページへ描画されます。

<body>
    <div style="page-break-after: always">
        Hello World
    </div>
    <div>
        Good Morning
    </div>
</body>

page-break-afterを指定しない場合

page-break-afterを指定した場合

ページ番号表示

laravel-dompdfのリポジトリ内で検索して見つけた方法を紹介します。

laravel-dompdfのオプションとしてPDFでPHPを実行できるというものがあります。

オプションを有効にするには以下の方法です。

$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('view')
    ->setOption('isPhpEnabled', true)
    ->stream();

PHPを有効化した上で以下のコードをbladeに記述します。

<script type="text/php">
    if (isset($pdf)) {
        $text = "page {PAGE_NUM} / {PAGE_COUNT}";
        $size = 10;
        $font = $fontMetrics->getFont("Verdana");
        $width = $fontMetrics->get_text_width($text, $font, $size) / 2;
        $x = ($pdf->get_width() - $width) / 2;
        $y = $pdf->get_height() - 35;
        $pdf->page_text($x, $y, $text, $font, $size);
    }
</script>

内容は変数名の通りですが、どういった文をどこに表示するのかを座標を使って指定しています。

あえて文字を大きくしていますが以下のように表示されます。

参考issueはこちら

CSSが不完全

HTMLからPDF出力するのでBootstrap等で簡単なレイアウトを作成すれば良いと考えていましたが、機能しないCSSがありうまくレイアウトを作成できませんでした。

横並びを素のCSSで表現するためにdisplay: flex;を使おうと思いましたが、これもうまく機能せずfloatを使って表現しました。

このように、このCSSは有効かどうかを確認しながら実装していく必要があるためそれほど複雑ではないレイアウトでも時間がかかってしまいました。

また、CSSファイルを外だししてheadタグで読み込むみたいなことを想定していましたが、これもうまく機能せず結局CSS用のbladeファイルを作成してそこにCSSをまとめるというような方法を取りました。

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    @include('pdfs.css')
    <title>PDF</title>
</head>

改行されない

通常HTMLであればテーブルで1つのセルの幅を超えそうな文を表示するときは自動的に改行されますが、それが実現されずPDFの幅を超える場合は見切れてしまうということが起きました。

とある記事でword-break:break-all;word-wrap:break-word;を使うことで改行されると書いてありましたが、設定が不足しているせいなのか私の環境では改行されませんでした。

textareaで入力された内容を見た目そのままPDFに表示する必要があったため、枠内に収まるように指定した文字数でbrタグを挿入する処理をPHP側で実装して対応しました。

横が見切れるという問題は解決されましたが、これによって問題が1つ生じます。

URLが挿入されていると指定文字数でbrタグが挿入されるため見た目は改行されたURLですが、実際には改行されるまでの文字列がURLとなるため無効なURLになってしまいます。

Webページの場合はtextareaにURLが入力されてそのまま表示してもただの文字列となりますが、PDFの場合はaタグで囲っていなくてもURLが挿入されていれば自動的にリンク化されてしまいます。

この問題を解決するために表示する文章の中にURLがあればaタグのhref属性に指定し、表示する文字は代わりの文字を使うといったことで対応しました。

以下がイメージ

<a href="https://r-tech14.com/hoge/hoge?hoge=hoge">https://r-tech14.com/hoge...</a>

PDFの高さを超えると文字が見切れる

改行されない問題は解決しましたが、文字数が多く、1枚のPDFに文章が収まらない場合にはみ出た分の文字が見切れてしまうということが起きました。

page-break-after: always;を指定することでタグが分かれていれば改ページされましたが、1つのタグであればはみ出た分の文章は改ページされませんでした。

対応としては1枚のPDFに収まる行数を確認して、表示するべき文章を収まる行ごとに配列に格納するという方法を取りました。

$array = [
    '親譲りの無鉄砲で小供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間ほど腰を抜かした事がある。',
    'なぜそんな無闇をしたと聞く人があるかも知れぬ。別段深い理由でもない。',
    '新築の二階から首を出していたら、同級生の一人が冗談に、いくら威張っても、そこから飛び降りる事は出来まい。',
    '弱虫やーい。と囃したからである。',
    '小使に負ぶさって帰って来た時、おやじが大きな眼をして二階ぐらいから飛び降りて腰を抜かす奴があるかと云ったから、この次は抜かさずに飛んで見せますと答えた。(青空文庫より)',
];

この配列をbladeの中でforeachで要素を1つずつタグ内に格納していくというイメージです。

タグが分かれていればそのページに収まらない分は次ページで表示されます。

まとめ

いろいろとlaravel-dompdfを使って対応しないといけないことや困ったことを紹介してきましたが、PDFをただ出力するだけであれば楽なライブラリかなと思います。

しかし思っているようにHTMLをPDFに変換してくれるわけではないので、設定を変更したり多少の工夫が必要です。

Laravelだとlaravel-snappyというライブラリも存在し、こちらはlaravel-dompdfよりはCSSに対応しているみたいです。

laravel-snappyもいろいろ検証してみたいなと思いました。

コメント

タイトルとURLをコピーしました