LaravelとLivewireを活用した画像保存法。アスペクト比とキャンバス余白の設定からS3へのアップロードまで


## 実装したいこと
画像を保存した後、Tailwind CSSを使用して横幅と高さを指定すると、指定されたサイズに合わせて画像が不自然に引き伸ばされてしまいました。そこで、画像のアスペクト比を保持しつつ、指定した横幅と高さで自然に表示されるように設定しました。余白はグレーで埋めることにしました。
元の画像
<img width="100" alt="スクリーンショット 2024-06-08 7.24.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/06459931-ca32-ecd0-f0e2-14fba21aa061.png">


表示時は不自然に引き伸ばされてしまう
```php
<img src="{{  $user->getFirstMediaUrl('images') }}" alt="user_image" class="w-[734px h-60">
```
<img width="300" alt="スクリーンショット 2024-06-08 7.24.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/5a23b497-bfef-dbe5-66ef-0f955c40c2c5.png">

## 使用技術
- Laravel
- [Livewire](https://livewire.laravel.com/)
- [spatie/laravel-medialibrary](https://spatie.be/docs/laravel-medialibrary/v11/introduction)
- [intervention/image](https://image.intervention.io/v3)
- S3

## 実装
### 1.必要なものをインストール
**Livewire**
```zsh
composer require livewire/livewire
```
**laravel-medialibrary**
```zsh
composer require "spatie/laravel-medialibrary"
```
必要なマイグレーションを実行してデータベースを設定します。`media`テーブルが作成されます。
```zsh
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="migrations"
php artisan migrate
```
作成されたテーブル
|カラム  |データ型  |説明  |
|---|---|---|
|id  |uuid  |PK  |
|model_type  |string  |メディアが関連づけられているモデルのタイプ  |
|model_id |integer  |FK。 メディアが関連付けられているモデルのid  |
|collection_name  |string  |メディアが属するコレクションの名前  |
|name  |string  |ファイルの名前  |
|file_name  |string  |実際のファイル名  |
|mime_type|string|ファイルのMIMEタイプ|
|disk|string|ファイルが保存されているディスクの名前|
|conversions_disk|string|変換されたメディアファイルが保存されているディスクの名前を保存
|size|bigint|ファイルのサイズ(バイト単位)|
|manipulations|json|メディアに適応された変換や操作の配列
|custom_properites|json|任意のプロパティ
|responsive_images|json|レスポンシブ画像の生成に関するデータ
|generated_conversions|json|画像変換をJSON形式で保存
|order_column|integer|同じコレクション内でのメディアの順序

User モデルでメディアを扱うために、`HasMedia` インターフェイスと `InteractsWithMedia` トレイトを実装し、モデルにメディアを添付できるようにします。
```Models.user.php
<?php

namespace App\Models;

use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Illuminate\Foundation\Auth\User as Authenticatable;


class User extends Authenticatable implements HasMedia
{
 use InteractsWithMedia;
```
**intervention/image**
```zsh
composer require intervention/image
```
**S3を利用してファイル操作を行うための**
```
composer require league/flysystem-aws-s3-v3
```
### 手順1.画像加工処理
画像加工処理はintervention/imageのResize機能を使用します。
今回はリサイズと余白埋めをするため`contain`を使用します。
#### 引数
```
public Image::contain(int $width, int $height, $background = 'ffffff', string $position = 'center'): ImageInterface
```
#### 使用例(公式から)
```php
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;

// create new image instance (800 x 600)
$image = ImageManager::withDriver(Driver::class)->read('images/example.jpg');

// resize padded without upsizing
$image->contain(900, 600);

// padded resizing with grey background color
$image->contain(500, 500, 'efefef');
```
https://image.intervention.io/v3/modifying/resizing

#### 実際の使用方法
```Livewire/UploadImage.php
    $manager = new ImageManager(new Driver());

    $image = $manager->read($this->newImage->get());

    // アスペクト比を変更せず、リサイズ。足りない箇所はグレーの背景で埋める
    $image->contain(734, 240, background: 'f5f5f5', position: 'center');
            
```
画像は以下のようになります
<img width="300" alt="スクリーンショット 2024-06-08 7.24.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/acd53fc9-854c-28b9-4686-5ee59545341b.png">

### 手順2.画像保存処理
S3に保存するためにはLaravelのStorageファサードを使用し、保存後のファイルをspatie/laravel-medialibraryを用いてユーザーのメディアコレクションに登録します。
```php
$filename = 'user/images/' . uniqid();
// S3に画像データを保存
Storage::disk('s3')->put($filename, $image->encode());
// 保存した画像のURLを取得
$full_path = Storage::disk('s3')->url($filename);

$user->clearMediaCollection('images');
$user->addMediaFromUrl($full_path)->toMediaCollection('images', 's3');
```
#### 1.ファイル名の生成
```php
$filename = 'user/images/' . uniqid();
```
ここでは、uniqid() 関数を使用して一意のIDを生成し、それをファイル名の一部として組み込んでいます。これにより、S3上でファイルが一意に識別され、他のファイルと名前が重複しないようになります。

#### 2.S3にデータを保存
```php
Storage::disk('s3')->put($filename, $image->encode());
```
指定された`$filename`で画像データをS3に保存しています。ここで`$image->encode()`は、画像を適切な形式（通常はJPEGやPNG）にエンコードするために使用されます。`Storage::disk('s3')`は、ファイル操作のためのS3ディスクインスタンスを指定しています。
#### 3.保存した画像のURLを取得
```php
$full_path = Storage::disk('s3')->url($filename);
```
#### 4.ユーザーのメディアコレクションをクリアし、新しい画像を追加
```php
$user->clearMediaCollection('images');
$user->addMediaFromUrl($full_path)->toMediaCollection('images', 's3');
```
最初に、ユーザーのimagesメディアコレクションをクリアしています。これは、古い画像を削除し、新しい画像のみを保持するためです。その後、取得した画像のURLを使用して新しい画像をメディアコレクションに追加しています。`toMediaCollection('images', 's3')`は、画像をimagesコレクションに追加し、s3をディスクとして指定しています。



#### 全体のコード
```Livewire/UploadImage.php
<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Livewire\WithFileUploads;

use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;

class UploadImage extends Component
{
    use WithFileUploads;

    public $newImage;

    /**
     * バリデーションルール
     */
    protected $rules = [
        'newImage' => 'nullable|image',
    ];

    /**
     * 画像のアップロード処理
     */
    public function saveImages()
    {
        $this->validate();

        $user = auth()->user();

        $manager = new ImageManager(new Driver());

        // メイン画像の保存処理
        $image = $manager->read($this->newImage->get());

        // アスペクト比を変更せず、リサイズ。足りない箇所はグレーの背景で埋める
        $image->contain(734, 240, background: 'f5f5f5', position: 'center');

        $filename = 'user/images/' . uniqid();
        // S3に画像データを保存
        //加工した画像のエンコードを引数として渡す
        Storage::disk('s3')->put($filename, $image->encode());

        // 保存した画像のURLを取得
        $full_path = Storage::disk('s3')->url($filename);

        $user->clearMediaCollection('images');
        $user->addMediaFromUrl($full_path)->toMediaCollection('images', 's3');

        return redirect()->route('user.profile');
    }

     public function render()
    {
        return view('livewire.user.upload-image');
    }
```