Webで作るカメラアプリ

将来的にPWAがiOSでも全実装されることを期待して、Webでカメラアプリを作ってみようと思い作ってみました。

# お題
拙作ノスタルジックフォトフィルターをWebアプリ化します。
もう9年前なんですねー懐かしい。
※アプリは配布を終了しております。

<img width="320" alt="IMG_0147.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214400/aaae5428-4900-3d9a-1797-49d3abe470a1.png">

# ソース
ここにあります。
https://github.com/miura333/nosfilter-pwa2

# 開発環境

| 項目 | 内容 | 備考 |
|:-----------------|:------------------|:------------------|
| サーバー | ロリポップ | 独自ドメイン取得、無料SSL |
| 言語 | PHP7.1 |  |
| フレームワーク | Laravel 5.7.13 |  |
| DB | 使用せず |  |
| 画像処理ライブラリ | GD |  |
| 検証端末 | iOS12.0.3, 11.3 |  |


ロリポ環境にLaravel入れて動作させています。
全然フレームワークのメリットを活かしていませんすいません。

# 前提条件

- PHPで書きます
- サーバー内になるべくファイルを残さないようにします

# 解説

## カメラから画像取得

- カメラですが、manifest.jsonのdisplayがfullscreen、またはmetaタグのapple-mobile-web-app-capableをyesに設定し、フルスクリーンで表示させようとするとエラーとなりカメラ映像を表示させることができませんでした。
- SafariのPWA対応、というよりはフルスクリーンか否かによって動作のする、しないが変わるようです。

```json:manifest.json
{
    "name": "nosfilter-pwa",
    "short_name": "nosFilter",
    "background_color": "#ffffff",
    "icons": [{
        "src": "./icon-152.png",
        "sizes": "152x152",
        "type": "image/png"
    },{
        "src": "./icon-120.png",
        "sizes": "120x120",
        "type": "image/png"
    }],
    "start_url": "./?utm_source=homescreen",
    "display": "browser",
    "theme_color": "#f7f7f7"
}
```

- HTML側ですが、ボタン押下後にカメラ映像をcanvasに描画し、それをjpeg形式で出力しています。
- 描画する前にカメラの縦横サイズとcanvasのサイズを同じにします。

```html:index.blade.php
<body class="cameraBody">
    <video id="video" autoplay playsinline></video>
    <div class="cameraSpacer"></div>
    <div class="cameraButtonParent">
        <div class="cameraButtonParent2">
            <input id="btnPicture" type="button" disabled="true" value="OK" class="cameraBtn"></input>
        </div>
    </div>
    <canvas id="imageCanvas" style="display:none;" width="300" height="300"></canvas>
    {!! Form::open(['url' => '/result', 'id' => 'formResult']) !!}
    {!! Form::hidden('imageData', null, ['id' => 'imagePost']) !!}
    {!! Form::close() !!}

    <script>
        const medias = {audio : false, video : {
            facingMode : {
              exact : "environment" // リアカメラにアクセス
            }
        }},
        video  = document.getElementById("video");

        navigator.getUserMedia(medias, successCallback, errorCallback);

        var width = 0, height = 0;

        video.addEventListener( "loadedmetadata", function (e) {
            width = this.videoWidth;
            height = this.videoHeight;
        }, false );

        function successCallback(stream) {
            video.srcObject = stream;

            var button = document.getElementById("btnPicture");
        	var canvas = document.getElementById("imageCanvas");

            button.disabled = false;
    		button.onclick = function() {
                canvas.width = width;
                canvas.height = height;
    		    canvas.getContext("2d").drawImage(video, 0, 0, width, height, 0, 0, width, height);
                var img = canvas.toDataURL("image/jpeg");
                
                $('#imagePost').val(img);
                $('#formResult').submit();
    		};
        };

        function errorCallback(err) {
            alert(err);
        };
    </script>
</body>
```

## 画像処理

- 画像処理はFormで受け取ったデータをファイル保存、そのファイルをGDで読み込んでいます。
- GDでデータから読み込む方法が分からなかったための苦肉の策。最終的にファイルは削除しています。
- あとはGD使ってピクセルデータにアクセスし画像処理の流れです。

以下はソースの一部、グレースケール化している箇所の抜粋です。

```ｐｈｐ:imageController.php

$img = str_replace('data:image/jpeg;base64,', '', $request->imageData);
$img = str_replace(' ', '+', $img);
$data = base64_decode($img);
$timestamp = time();
$file = $timestamp . ".jpeg";
Storage::disk('local')->put('public/'.$file, $data);
$storagePath = asset('storage/');
Log::debug($storagePath);

$src = imagecreatefromjpeg('storage/'.$file);

$imageWidth = imagesx($src);
$imageHeight = imagesy($src);

$grayImage = imagecreatetruecolor($imageWidth, $imageHeight);

for($i = 0; $i < $imageWidth; $i++){
    for($j = 0; $j < $imageHeight; $j++){
        //get pixel
        $rgb = imagecolorat ($src, $i, $j);
        $r = ($rgb >> 16) & 0xFF;
        $g = ($rgb >> 8) & 0xFF;
        $b = $rgb & 0xFF;

        $gray = ($r*0.125)+($g*0.25)+($b*0.0625);
        $dstColor = imagecolorallocate($grayImage, $gray, $gray, $gray);
        imagesetpixel($grayImage, $i,$j, $dstColor);
    }
}

```

## 画像データ生成

- 画像をjpeg保存しそれをbase64エンコード
- 先頭に引数で来た時と同様のヘッダを付与しViewに渡します。このヘッダが無いとimgタグで表示されません。

```ｐｈｐ:imageController.php
imagejpeg($resultImage, 'storage/'.$fileOut, 80);

$dataOut = file_get_contents('storage/'.$fileOut);
$imageData64 = base64_encode($dataOut);

unlink('storage/'.$fileOut);

return view('result')->with(['imageData' => 'data:image/jpeg;base64,'.$imageData64, 'width' => $imageWidth, 'height' => $imageHeight]);
```

できた

![IMG_8220.JPG](https://qiita-image-store.s3.amazonaws.com/0/214400/f2ea02b6-9624-2ea5-60f7-01ef320e103e.jpeg)

## 感想
- 遅い。Wi-fiなら兎も角4G回線で試すとシャッター押下後の動作が非常に重いです。
- 当初pngでデータ取得をしていたのですが、jpegに変えても体感的にそれほど変わりませんでした。
- javascript版も作ってみようと思いますが、仮に商用利用を考えた場合にjavascriptだと画像処理部分のソースをコピーできてしまうのでどうしようかなあと。
- とりあえず写真を撮影してピクセルデータをいじってフィルターかけるくらいは出来る、ということが分かりました。写真をアップするためだけにアプリを作って欲しいみたいな依頼が来たら提案してみようかと。
- 画像処理サーバーはメモリ消費がやばそう。

# 参考
https://qiita.com/umamichi/items/0e2b4b1c578e7335ba20
https://qiita.com/mu_tomoya/items/d35dbb580a561c562ca6
http://kimizuka.hatenablog.com/entry/2017/11/06/140337