1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

複数PDFを1ファイルに結合&一括印刷する方法【Laravel + Browsershot】

Posted at

🧾 やりたいこと

Laravelアプリ上でチェックした複数のPDFを

・1つに結合

・自動で表示

・自動で印刷ダイアログ表示

する仕組みを作ります。

🧰 使用技術

Laravel 9.x

Browsershot(spatie/browsershot)

Node.js + Puppeteer

setasign/fpdi(PDF結合)

JavaScript(iframe onload → print)

📦 1. インストール

✅ Node.js & Puppeteer インストール手順

✅ Laravel側に Browsershot 導入

✅ fpdf & fpdi 導入

ライブラリ 主な用途
setasign/fpdf PDFの新規作成(ゼロから)
setasign/fpdi 既存PDFの読み込み・結合・ページ抽出など
fpdifpdfに依存 → 両方インストールが基本(fpdiだけでは動かない)
composer require setasign/fpdi
composer require setasign/fpdf

🖨️ 2. 印刷ボタン

/resources/views/receipts/index.blade.php
<form id="receipt-form" method="POST" target="_blank">
    @csrf

    <div class="flex gap-2 mb-4">
        <button 
            type="submit"
            onclick="submitForm('{{ route('receipts.bulkDownload') }}')"
            class="text-white bg-gray-500 px-4 py-2 rounded hover:bg-gray-600">
            ✅ 選択したPDFを一括DL
        </button>

        <button 
            type="submit"
            onclick="submitForm('{{ route('receipts.generate_and_print_multiple') }}')"
            class="text-white bg-green-500 px-4 py-2 rounded hover:bg-green-600">
            🖨️ 選択したPDFを一括印刷
        </button>
    </div>

    {{-- ダウンロードチェックボックスエラーメッセージ --}}
    @if(session('error'))
        <div id="flash-message-error" class="text-red-500 mb-2">{{ session('error') }}</div>
    @endif

    {{-- 全て選択ボタン --}}
    <div class="text-right">
        <label class="inline-flex items-center cursor-pointer">
            <input type="checkbox" id="select-all" class="form-checkbox text-indigo-600 cursor-pointer">
            <span class="ml-1">すべて選択 / 解除</span>
        </label>
    </div>

    <table class="whitespace-nowrap table-auto w-full text-left whitespace-no-wrap">
        <thead>
            <tr>
                <th
                    class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100 rounded-tr-lg"></th>
            </tr>
        </thead>
        <tbody>
            @foreach($receipts as $receipt)
                <tr>
                    {{-- チェックボックス --}}
                    <td class="border-t-2 border-gray-200 px-4 py-3">
                        <input type="checkbox" name="receipt_ids[]" value="{{ $receipt->id }}" class="cursor-pointer">
                    </td>
                </tr>
            @endforeach
        </tbody>
    </table>
</form>

🏗 3. ルーティング設定

web.php
Route::post('/receipts/pdf/print-multiple', [ReceiptController::class, 'generateAndPrintMultiple'])->name('receipts.generate_and_print_multiple');
Route::get('/receipts/print/show/{filename}', [ReceiptController::class, 'showPrintView'])->name('receipts.print.show')

🧑‍💻 4. # tmpディレクトリ(storage/app/public/tmp)作成

🧠 5. Controller側の実装:選択された複数の領収書をPDF化して1つに結合し、印刷用の中継画面にリダイレクト

/Http/Controllers/ReceiptController.php
use App\Http\Requests\ReceiptRequest;
use App\Models\PaymentMethod;
use App\Services\ReceiptService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
use Spatie\Browsershot\Browsershot;
use ZipArchive;
use setasign\Fpdi\Fpdi;

// ⭐️ 選択された複数の領収書をPDF化して1つに結合し、印刷用の中継画面にリダイレクトする
public function generateAndPrintMultiple(Request $request)
{
    // ✅ 情報を取得
    $ids = $request->input('receipt_ids', []);
    if(empty($ids)) {
        return back()->with('error', '印刷する領収書を選択してください。');
    }

    /** @var \App\Models\User $user */
    $user = Auth::user();

    // ✅ 選択された各領収書を`HTML`から`PDF`に変換して一時保存し、ファイル名を配列にまとめている
    $filenames = [];
    foreach($ids as $id) {
        $receipt = $user->receipts()->with(['paymentMethod', 'bentoDetails'])->findOrFail($id);
        $html = view('pdf.receipt', compact('receipt'))->render();

        $customerName = preg_replace('/[^\w\-]/u', '_', $receipt->customer_name);
        $filename = "receipt_{$customerName}_{$id}.pdf";
        $pdfPath = storage_path("app/public/tmp/{$filename}");

        Browsershot::html($html)
            ->setNodeBinary('/usr/local/bin/node')
            ->setIncludePath('/usr/local/bin')
            ->format('A4')
            ->showBackground()
            ->save($pdfPath);

        $filenames[] = $filename;
    }

    // ✅ PDFファイルの絶対パス(結合用に必要)
    $pdfPaths = array_map(function ($filename) {
        return storage_path("app/public/tmp/{$filename}");
    }, $filenames);

    // ✅ 結合後のPDF保存先
    $mergedFilename = 'merged_receipt.pdf';
    $mergedPath = storage_path("app/public/tmp/{$mergedFilename}");

    // ✅ 結合処理
    $this->mergePdfs($pdfPaths, $mergedPath); // $this = `generateAndPrintMultiple()メソッド`が定義されているクラス

    // ✅ 中継ビューへリダイレクト(iframe + 印刷)
    return redirect()->route('receipts.print.show', ['filename' => $mergedFilename]);
}

// ⭐️ 複数のPDFファイルを1つに結合して指定パスに保存する
public function mergePdfs(array $pdfPaths, string $mergedPath)
{
    // ✅ Fpdfを継承したFpdiインスタンスを作成
    $pdf = new class extends Fpdi {
        // 何も追加しなくてOK(匿名クラス)
    };

    foreach($pdfPaths as $file) {
        // 🔹 読み込むPDFファイルを指定して、ページ数などの情報を取得
        $pageCount = $pdf->setSourceFile($file);

        // 🔹 `$pdfPaths`の中にある`PDF($file)`の各ページを読み込んで、新しいPDFに1ページずつ同じサイズで追加
        for($pageNo = 1; $pageNo <= $pageCount; $pageNo++) { // 例)A領収書:1ページ、B:1,B:2、C:1
            $tplIdx = $pdf->importPage($pageNo); // 「$pageNo ページ目をコピー機に乗せる準備をする」
            $size = $pdf->getTemplateSize($tplIdx);

            $pdf->AddPage($size['orientation'], [$size['width'], $size['height']]);
            $pdf->useTemplate($tplIdx);
        }
    }

    // ✅ 作ったPDFを保存
    $pdf->Output('F', $mergedPath); //(ファイルとして保存, ファイルパス)
}

🖼 6. 中継ビュー(PDF表示&印刷)

🧠 7. 中継ビュー用 Controller を追加

🗑 8. 一時PDFのクリーンアップ(任意)

本番でインストール必須

① PHPパッケージ(Composer)

パッケージ名 用途
spatie/browsershot HTML → PDF化(Puppeteer使う)
setasign/fpdi 複数PDFを結合
illuminate/support Laravelコア(通常すでに入ってる)
# 本番環境で依存パッケージをインストール(最適化あり)
composer install --no-dev --optimize-autoloader

# 必要なら個別に(まだ入れてない場合)
composer require spatie/browsershot
composer require setasign/fpdi
composer require setasign/fpdf

② Node.js & Puppeteer

ツール 用途
Node.js Puppeteerを動かすJS実行環境
Puppeteer(Headless Chrome) ブラウザレンダリングしてPDF生成
# Node.js(v18など安定版)をインストール
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs

# Puppeteerをグローバルにインストール(またはプロジェクトに)
npm install -g puppeteer

ストレージディレクトリの準備

PDFの一時保存先 /storage/app/public/tmp に書き込める必要あり。

# 一時PDFを保存するディレクトリを作成
mkdir -p storage/app/public/tmp

# 書き込み権限を付与(LaravelのWebサーバーユーザーに応じて調整)
chmod -R 775 storage/app/public/tmp

古いPDFを自動削除(スケジュール登録)

関連

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?