はじめに
実務で画像のリサイズや圧縮などの処理について学ぶ必要があったため、Laravelでの基本的な画像処理を備忘録としてまとめておきます。
画像処理を行う理由:
- ストレージ容量の削減
- ページの読み込み速度の向上
- 適切なサイズでの表示
Intervention Imageの導入
Laravelで画像処理を行う際は、Intervention Imageというライブラリがよく使われます。
インストール
composer require intervention/image
設定
config/app.phpに追加します(Laravel 10以前の場合)。
'providers' => [
// ...
Intervention\Image\ImageServiceProvider::class,
],
'aliases' => [
// ...
'Image' => Intervention\Image\Facades\Image::class,
],
Laravel 11以降では自動的に読み込まれます。
基本的な画像のアップロードと保存
まずは基本的なアップロード処理から。
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
public function store(Request $request)
{
$request->validate([
'image' => 'required|image|mimes:jpeg,png,jpg|max:10240', // 最大10MB
]);
// storage/app/public/images に保存
$path = $request->file('image')->store('images', 'public');
return response()->json([
'path' => $path,
'url' => Storage::url($path)
]);
}
シンボリックリンクの作成も必要です。
php artisan storage:link
画像のリサイズ
アップロードされた画像をリサイズして保存します。
use Intervention\Image\Facades\Image;
use Illuminate\Support\Str;
public function store(Request $request)
{
$request->validate([
'image' => 'required|image|mimes:jpeg,png,jpg|max:10240',
]);
$image = $request->file('image');
$filename = time() . '_' . Str::random(10) . '.' . $image->extension();
// 画像を読み込み
$img = Image::make($image->getRealPath());
// リサイズ(幅800px、高さは自動計算)
$img->resize(800, null, function ($constraint) {
$constraint->aspectRatio(); // アスペクト比を維持
$constraint->upsize(); // 元のサイズより大きくしない
});
// 保存
$img->save(storage_path('app/public/images/' . $filename));
return response()->json(['filename' => $filename]);
}
リサイズのパターン
// 幅と高さを指定(アスペクト比は無視)
$img->resize(800, 600);
// 幅のみ指定(高さは自動)
$img->resize(800, null, function ($constraint) {
$constraint->aspectRatio();
});
// 高さのみ指定(幅は自動)
$img->resize(null, 600, function ($constraint) {
$constraint->aspectRatio();
});
// 元のサイズより大きくしない
$img->resize(800, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
画像の圧縮
ファイルサイズを削減するために圧縮を行います。
// JPEG形式で保存(品質を指定)
$img->save(storage_path('app/public/images/' . $filename), 80); // 品質80%
// PNG形式で保存
$img->encode('png', 75)->save(storage_path('app/public/images/' . $filename));
品質の目安:
- 90-100: 高品質(ファイルサイズ大)
- 80-85: バランスが良い(推奨)
- 60-75: 低品質(ファイルサイズ小)
実装では品質80-85%で十分なケースが多いです。
古い画像の削除
更新時に古い画像を削除します。
use Illuminate\Support\Facades\Storage;
public function update(Request $request, $id)
{
$request->validate([
'image' => 'nullable|image|mimes:jpeg,png,jpg|max:10240',
]);
$record = ImageModel::findOrFail($id);
// 新しい画像がアップロードされた場合
if ($request->hasFile('image')) {
// 古い画像を削除
if ($record->image_path) {
Storage::disk('public')->delete('images/' . $record->image_path);
}
// 新しい画像を保存
$image = $request->file('image');
$filename = time() . '_' . Str::random(10) . '.jpg';
$img = Image::make($image->getRealPath());
$img->resize(800, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$img->save(storage_path('app/public/images/' . $filename), 80);
$record->image_path = $filename;
}
$record->save();
return response()->json($record);
}
画像形式の変換
異なる画像形式に変換します。
// PNGをJPEGに変換
$img = Image::make($image->getRealPath());
$img->encode('jpg', 85);
$img->save(storage_path('app/public/images/' . $filename));
// WebP形式に変換
$img = Image::make($image->getRealPath());
$img->encode('webp', 80);
$img->save(storage_path('app/public/images/' . $webpFilename));
PDFを画像に変換
PDFファイルを画像として保存する場合、Imagickが必要です。
public function convertPdfToImage(Request $request)
{
$request->validate([
'pdf' => 'required|mimes:pdf|max:10240',
]);
$pdf = $request->file('pdf');
$filename = time() . '_' . Str::random(10) . '.jpg';
try {
$imagick = new \Imagick();
$imagick->setResolution(150, 150); // 解像度を設定
$imagick->readImage($pdf->getRealPath() . '[0]'); // 1ページ目を読み込み
$imagick->setImageFormat('jpg');
$imagick->writeImage(storage_path('app/public/images/' . $filename));
$imagick->clear();
$imagick->destroy();
return response()->json(['filename' => $filename]);
} catch (\Exception $e) {
return response()->json(['error' => 'PDFの変換に失敗しました'], 500);
}
}
注意: ImagickはGDとは別のライブラリで、サーバー環境によってはインストールされていない場合があります。共用サーバーでは使用できないことが多いため、フロントエンド側での対応も検討する必要があります。
Serviceクラスでの実装例
画像処理をServiceクラスに分離すると、コードの再利用性が高まります。
// app/Services/ImageService.php
namespace App\Services;
use Intervention\Image\Facades\Image;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ImageService
{
public function resize($file, $width = 800, $quality = 80)
{
$filename = time() . '_' . Str::random(10) . '.jpg';
$img = Image::make($file->getRealPath());
$img->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$img->save(storage_path('app/public/images/' . $filename), $quality);
return $filename;
}
public function delete($filename)
{
if ($filename) {
Storage::disk('public')->delete('images/' . $filename);
}
}
}
コントローラーでの使用:
use App\Services\ImageService;
public function store(Request $request, ImageService $imageService)
{
$request->validate([
'image' => 'required|image|mimes:jpeg,png,jpg|max:10240',
]);
$filename = $imageService->resize($request->file('image'), 800, 80);
return response()->json(['filename' => $filename]);
}
実装時の注意点
実際に実装する中で遭遇した注意点をまとめます。
1. HEIC形式などの画像変換
iPhoneで撮影した画像(HEIC形式)は、サーバー環境によっては変換ライブラリがない場合があります。特に共用サーバーでは、libheifが使用できないことがあります。
解決方法: フロントエンド側で事前に変換する
// heic2anyライブラリを使用
import heic2any from 'heic2any';
const fileInput = document.querySelector('#image-input');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file.type === 'image/heic' || file.name.toLowerCase().endsWith('.heic')) {
try {
const convertedBlob = await heic2any({
blob: file,
toType: 'image/jpeg',
quality: 0.8
});
const convertedFile = new File(
[convertedBlob],
file.name.replace(/\.heic$/i, '.jpg'),
{ type: 'image/jpeg' }
);
// 変換後のファイルを使用
uploadFile(convertedFile);
} catch (error) {
console.error('変換エラー:', error);
}
} else {
uploadFile(file);
}
});
2. 大きい画像を複数枚アップロードする際のリクエストサイズ制限
サイズの大きい画像を複数枚アップロードする際に、PHPのpost_max_size制限に引っかかる場合があります。
解決方法1: フロントエンド側で圧縮してから送信
// browser-image-compressionライブラリを使用
import imageCompression from 'browser-image-compression';
const handleImageUpload = async (files) => {
const options = {
maxSizeMB: 1,
maxWidthOrHeight: 1920,
useWebWorker: true
};
const compressedFiles = [];
for (const file of files) {
try {
const compressedFile = await imageCompression(file, options);
compressedFiles.push(compressedFile);
} catch (error) {
console.error('圧縮エラー:', error);
}
}
// 圧縮後のファイルを送信
const formData = new FormData();
compressedFiles.forEach((file, index) => {
formData.append(`images[${index}]`, file);
});
await fetch('/api/images', {
method: 'POST',
body: formData
});
};
解決方法2: サーバー側のリクエストサイズ制限を上げる
// php.iniまたは.htaccessで設定
post_max_size = 50M
upload_max_filesize = 50M
ただし、共用サーバーでは制限を変更できない場合があるため、フロントエンド側での圧縮が確実です。
今後の改善案: 非同期での画像処理
現在の実装では、画像処理が完了するまでユーザーが待つ必要があります。大量の画像や重い処理の場合、キュー(Job)を使用して非同期で処理することで、ユーザー体験を向上できます。
Jobの作成
php artisan make:job ProcessImage
Job実装例
// app/Jobs/ProcessImage.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Intervention\Image\Facades\Image;
class ProcessImage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $imagePath;
protected $filename;
public function __construct($imagePath, $filename)
{
$this->imagePath = $imagePath;
$this->filename = $filename;
}
public function handle()
{
$img = Image::make($this->imagePath);
$img->resize(800, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$img->save(storage_path('app/public/images/' . $this->filename), 80);
// 一時ファイルを削除
unlink($this->imagePath);
}
}
コントローラーでの使用
use App\Jobs\ProcessImage;
public function store(Request $request)
{
$request->validate([
'image' => 'required|image|mimes:jpeg,png,jpg|max:10240',
]);
$image = $request->file('image');
$filename = time() . '_' . Str::random(10) . '.jpg';
// 一時的に保存
$tempPath = $image->storeAs('temp', $filename, 'local');
// 非同期で画像処理
ProcessImage::dispatch(storage_path('app/' . $tempPath), $filename);
return response()->json([
'message' => '画像を処理中です',
'filename' => $filename
]);
}
この方法により、画像処理をバックグラウンドで実行し、ユーザーは即座にレスポンスを受け取れるようになります。今後、この実装を検討していきたいと考えています。
まとめ
Laravelでの基本的な画像処理について学んだことをまとめました。
押さえておくべきポイント:
- Intervention Imageを使えば簡単に画像処理ができる
- リサイズと圧縮で容量を削減できる
- 画像形式の変換でファイルサイズを最適化できる
- 古い画像の削除を忘れずに
- Serviceクラスで処理を分離すると再利用しやすい
- 場合によってはフロントエンド側で画像の圧縮や変換を行う必要がある
- キューを使用することで非同期で画像処理を行うことができる
実装する際は、保存先のディレクトリ権限やメモリ制限にも注意が必要です。画像処理はユーザー体験に直結する部分なので、適切に実装していきたいと思います。