
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、日頃業務を進めているとクライアント様のご希望に沿えなかったものの、
「うーん、これはシンプルに自分でやってみたい」
と感じるものが出てこないでしょうか。
とどのつまり「ボツ案」というやつなのですが、「仕様に合わなかっただけで他の場所では使えるんじゃないか」と思う機能なので、興味が出てしまうんですね。
そして、先日もそういった機能に出会いました。
それが・・・・・・
Excel や Word のサムネイルつくる
というものです。
そこで
今回は、Laravel
を使って「アップロードされた Office ファイルのサムネイルをつくる」という機能をつくってみたいと思います。
ぜひ何かの参考になりましたら嬉しいです。
※ ただし、今回のサムネイル作成はフリーソフトのlibreoffice
を使いますので、デザインやマクロなどを使った部分は完璧に再現できないことがあります。あらかじめご了承ください。m(_ _)m
「姪っ子が
真・三國無双の影響で
三国志好きになってました」
開発環境: Laravel 9.x、PHP 8.1、Ubuntu 20.20
目次 [非表示]
必要なパッケージをインストールする
まず、Excel
やWord
ファイルを画像化するのは以下の手順になります。
- いったん PDF 化する
- PDF を画像化する
- お好みでサイズを変更する
これを実現するためには、以下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-utils
のpdftoppm
コマンドを使うことにしました。
参考ページ: 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
の場合は透過になります。そのため、作成した画像は以下のようにIntervention
のcanvas
などを作って合成するといいでしょう。
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
で開くことができないのでうまくいきませんでした。もしパスワード付きで開く場合はlibreoffice
のmacro
を書けばいけるようです。
おわりに
ということで、今回はLaravel
を使ってOffice
ファイルのサムネイル化を実装してみました。
直接libreoffice
で画像化できればいいのですが、一旦PDF
を挟んでいるので手間といえば手間でしたが、ほぼパッケージが全てやってくれるので想像していたより簡単に実行することができました。
また、以前「Imagickは危険です」というような記事を読んだことがあるのですが、「そっか、今回みたいなパターンか」と伏線回収されたような気分でした(笑)
そして最後になりますが、今回のアイデアをくださった(記事にすることも了承していただいた)クライアント様に心より感謝です。m(_ _)m
ぜひ皆さんもやってみてくださいね。
ではでは〜
「姪っ子のために、
今度は戦国無双(中古)も
買いました」