【Laravel】Excel、Word、PowerPoint のサムネイルをアップロード時につくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、日頃業務を進めているとクライアント様のご希望に沿えなかったものの、

「うーん、これはシンプルに自分でやってみたい」

と感じるものが出てこないでしょうか。

とどのつまり「ボツ案」というやつなのですが、「仕様に合わなかっただけで他の場所では使えるんじゃないか」と思う機能なので、興味が出てしまうんですね。

そして、先日もそういった機能に出会いました。

それが・・・・・・

Excel や Word のサムネイルつくる

というものです。

そこで❗

今回は、Laravelを使って「アップロードされた Office ファイルのサムネイルをつくる」という機能をつくってみたいと思います。

ぜひ何かの参考になりましたら嬉しいです。😊✨

※ ただし、今回のサムネイル作成はフリーソフトのlibreofficeを使いますので、デザインやマクロなどを使った部分は完璧に再現できないことがあります。あらかじめご了承ください。m(_ _)m

「姪っ子が
真・三國無双の影響で
三国志好きになってました😆」

 

開発環境: Laravel 9.x、PHP 8.1、Ubuntu 20.20

必要なパッケージをインストールする

まず、ExcelWordファイルを画像化するのは以下の手順になります。

  1. いったん PDF 化する
  2. PDF を画像化する
  3. お好みでサイズを変更する

これを実現するためには、以下3つのパッケージが必要になります。

  • libreoffice: Ubuntu 側
  • poppler-utils: Ubuntu 側
  • Intervention: Laravel 側

ということで、以下のコマンドでインストールをしてください。(もしかしたらすでに入ってるかもです)

Laravel 側

composer require intervention/image

Ubuntu 側

sudo apt install libreoffice
sudo apt install poppler-utils

※ なお、元々はImagickを使って実装しようとしていましたが、PostScriptを使ったリスクが発生するとのことで、poppler-utilspdftoppmコマンドを使うことにしました。

📝 参考ページ: Solution to ImageMagick “not authorized” PDF Error

※ なお、Imagickを使って実装する場合は ちなみに:Imagick の許可をする をご覧ください。

ルートをつくる

では、今回は分かりやすいと思いましたので、まずはルートをつくっていきます。

routes/web.php

// 省略

use App\Http\Controllers\OfficeToImageController;

Route::prefix('office_to_image')->controller(OfficeToImageController::class)->group(function(){

    Route::get('create', 'create')->name('office_to_image.create');
    Route::post('/', 'store')->name('office_to_image.store');

});

コントローラーをつくる

続いてコントローラーです。
以下のコマンドを実行してください。

php artisan make:controller OfficeToImageController

すると、ファイルが作成されるので中身を以下のように変更します。

app/Http/Controllers/OfficeToImageController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Str;

class OfficeToImageController extends Controller
{
    public function create()
    {
        return view('office_to_image.index');
    }

    public function store(Request $request)
    {
        // 注意: バリデーションは省略しています!

        $file = $request->file('file');
        $extension = $file->getClientOriginalExtension();
        $filename = now()->format('Y-m-d') .'-'. Str::random(10) . '.' . $extension;
        $storage_path = $file->storeAs('office', $filename);
        $office_path = storage_path('app/' . $storage_path);
        $image_path = $this->convertOfficeToImage($office_path);

        return \Image::make($image_path)->response('png'); // 確認のため作成した画像を表示する
    }

    private function convertOfficeToImage($office_path)
    {
        $extension = pathinfo($office_path, PATHINFO_EXTENSION);
        $target_extensions = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];

        if(in_array($extension, $target_extensions, true)) {

            $pdf_path = $this->convertOfficeToPdf($office_path);
            return $this->convertPdfToImage($pdf_path);

        }

        return null;
    }

    private function convertOfficeToPdf($office_path)
    {
        putenv('HOME=/tmp'); // libreoffice の作業スペースとして tmp を使う

        $pdf_dir = storage_path('app/pdf');
        $command_parts = [
            'libreoffice',
            '--headless',
            '--convert-to pdf:writer_pdf_Export',
            '--outdir '. $pdf_dir,
            $office_path
        ];
        $command = implode(' ', $command_parts);
        exec($command);

        $filename = pathinfo($office_path, PATHINFO_FILENAME);
        $pdf_path = $pdf_dir . '/' . $filename . '.pdf';

        return file_exists($pdf_path) ? $pdf_path : null;
    }

    private function convertPdfToImage($pdf_path)
    {
        if(is_null($pdf_path)) {

            return null;

        }

        $filename = pathinfo($pdf_path, PATHINFO_FILENAME);
        $save_path = storage_path('app/images/' . $filename); // 最後に「.png」はつけない
        $command = 'pdftoppm '. $pdf_path .' -png -singlefile '. $save_path;
        exec($command);

        $image_path = $save_path .'.png';
        $this->correctImage($image_path);

        return file_exists($image_path) ? $image_path : null;
    }

    private function correctImage($image_path)
    {
        $width = 300;
        $img = \Image::make($image_path);
        $img->resize($width, null, function($constraint) { // 画像をリサイズ

            $constraint->aspectRatio();

        });
        $img->save();
    }
}

なお、この中で重要なのが、以下の部分です。

putenv('HOME=/tmp');

というのも、どうやらlibreofficeは作業をする際に隠しファイルを作るようなのですが、その権限がないためエラーになってしまう(PDFを作成できない)からです。

また、以下のPDFを画像化しているところは、セットする$save_pathには拡張子はつけないようにしてください。(自動でコマンドがセットしてくれるので重複してしまうからです)

$command = 'pdftoppm '. $pdf_path .' -png -singlefile '. $save_path;

ビューをつくる

では、最後にファイルを送信するフォームをつくりましょう。

resources/views/office_to_image/index.blade.php

<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="p-5">
        <h1>Officeファイル(Excel、Word、PowerPoint)をアップロードしてサムネイルを作成するサンプル</h1>
        <form method="POST" action="{{ route('office_to_image.store') }}" enctype="multipart/form-data">
            @csrf
            <div class="py-4">
                <input type="file" name="file">
            </div>
            <button class="btn btn-primary" type="submit">アップロードする</button>
        </form>
    </div>
</body>
</html>

※ ちなみに、久しぶりにAjaxではないアップロードをつくったので、enctype="multipart/form-data"@csrfのことをすっかり忘れてました😅

ちなみに:(ボツ案にした)Imagick の許可をする

Imagickは初期状態でPDFを扱おうとすると、「not authorized」とエラーが出てうまくいきません。

どうやらPostScript(アドビの開発している言語)の影響でPDFの取扱にはリスクが伴うことが理由のようです。

※ そのため、リスクをご理解した上で使ってください。

まずは設定ファイルを開いて、

sudo vi /etc/ImageMagick-6/policy.xml

以下のPDF部分に許可を与えます。

<policy domain="coder" rights="none" pattern="PDF" />

↓ ↓ ↓

<policy domain="coder" rights="read|write" pattern="PDF" />

これでOKです。

また、pdftoppmで画像化した場合、自動的に背景は白になりますが、Imagickの場合は透過になります。そのため、作成した画像は以下のようにInterventioncanvasなどを作って合成するといいでしょう。

private function correctImage($image_path)
{
    $width = 300;
    $img = \Image::make($image_path);
    $img->resize($width, null, function($constraint) {

        $constraint->aspectRatio();

    });
    $height = $img->height();

    $canvas = \Image::canvas($width, $height, '#ffffff');
    $canvas->insert($img, 'center'); // 👈 ここで合成
    $canvas->save($image_path);
}

テストしてみる

では、実際にテストしてみましょう❗

今回使うのは、内閣府が以下のページで公開している「内閣人事局経費(Excel形式:102KB)」というファイルです。

参考ページ: 令和3年度行政事業レビューシート 最終公表

まずは、「https://******/office_to_image/create」へブラウザでアクセスします。

そして、先ほどのExcelファイルを選択し、「アップロードする」ボタンをクリックすると、

どうなるでしょうか・・・・・・???

はい❗
サムネイルが表示されました(上の画像は、実際に作成したファイルです)

成功です😊✨

※ なお、やはりパスワード付きのファイルはlibreofficeで開くことができないのでうまくいきませんでした。もしパスワード付きで開く場合はlibreofficemacroを書けばいけるようです。

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はLaravelを使ってOfficeファイルのサムネイル化を実装してみました。

直接libreofficeで画像化できればいいのですが、一旦PDFを挟んでいるので手間といえば手間でしたが、ほぼパッケージが全てやってくれるので想像していたより簡単に実行することができました。

また、以前「Imagickは危険です」というような記事を読んだことがあるのですが、「そっか、今回みたいなパターンか❗」と伏線回収されたような気分でした(笑)

そして最後になりますが、今回のアイデアをくださった(記事にすることも了承していただいた)クライアント様に心より感謝です。m(_ _)m

ぜひ皆さんもやってみてくださいね。

ではでは〜❗

「姪っ子のために、
今度は戦国無双(中古)も
買いました😆」

このエントリーをはてなブックマークに追加       follow us in feedly