🙌

DjangoアプリケーションにCroppieを導入する(part2)

2021/01/24に公開

開発しているWebアプリケーションに
画像のクロッピング&アップロード機能としてCroppieを実装する話です。
前回の続きになります。

前回の記事はこちら
https://zenn.dev/to4yasuk2b/articles/2402cde872ae14

今回のスコープ

前回で、Bootstrap4のモーダル画面に埋め込んだフォームから元画像を登録すると、Croppieが起動するところまでをやりました。

今回は、Croppieでクロッピングした画像をサーバサイドへ保存する所までをご紹介します。
要は後半部分ということですね。

ロジックの構想

この手のクロッピング画像は、プロフィール画像として使用される事が殆どだと思います。
私のケースもそうでした。

その場合、登録した画像は直ちに画面上に反映された方がスマートです。
なので、Ajaxを使って非同期処理でサーバサイドにPOSTして、保存した画像のURLを戻して貰って画面更新するというロジックでいきたいと思います。

下記のような流れですね。

  1. Croppie上で登録ボタンを押下
  2. 画像がクロッピング画像されてサーバサイドへPOST ※Ajax非同期処理
  3. サーバサイドでPOSTされた画像をユーザ情報に紐づけて保存
  4. 3で保存した画像のURLをResponse
  5. 4で受けた画像URLを使って画面上を更新

問題発生

はい、いきなり問題です。
と、いうか、私がわかってなかっただけなんですが(涙)

何がかというと、Croppieから戻ってくるデータの形式です。
画像データはresultというメソッドで取得するのですが、その形式が下記の様になっています。

result({ type, size, format, quality, circle })

formatに画像形式を入れます。デフォルトがpngで、他にjpeg|png|webpが可能です。
詳細はCroppie公式を確認ください。

この時点で、私は指定した形式の画像データをバイナリで返してくれるものと思っていました。
わかってる方からするとお前はアフォかという話です。
はいすみません。修行が足りません。

メソッドの仕様をよく読めば分かるんですが、このライブラリはクロッピングした画像をtypeで指定した形式で返します。で、そのデフォルトはcanvasです。
他にもいろいろありますが、どれもブラウザ上でHTMLに組み込んで使うための形式になっています。
決して、Windowsのペイントアプリのトリミング⇒Ctrl+Sみたいなことは出来ない訳です。
ペイントアプリのクロッピング
こういうことは出来ませんよ

JSのライブラリなんだから、まあ、考えれば当たり前ですね。

どうするか

先ほどのtypeパラメータにbase64を指定すると、データ形式をBase64形式で返してくれます。
そこで、それを一旦そのままPOSTし、サーバサイドでデコードすることにしました。

POST処理の実装

と、いうことで、POST処理です。
part1でご紹介したコードは省略しています。

myapp_template.html
<script>
    // CSRFトークンに関する処理
    function getCookie(name) {
        let cookieValue = null;
        if (document.cookie && document.cookie !== '') {
            const cookies = document.cookie.split(';');
            for (let i = 0; i < cookies.length; i++) {
                const cookie = cookies[i].trim();
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) === (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
    const csrftoken = getCookie('csrftoken');

    function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }
    
    // 登録ボタン押下
    $('#post_image').click(function(event){
        var csrf_token = getCookie("csrftoken");
        var rslt = window.confirm("プロフィール画像を登録してよろしいですか?");
        if (rslt) {
            $image_crop.croppie('result', 'base64').then(function(response){
                $.ajax({
                    url:"{% url 'myapp:image_upload' %}",
                    type: "POST",
                    data:{"image": response},
                    dataType: 'json',
                    // 送信前にヘッダにcsrf_tokenを付与
                    beforeSend: function(xhr, settings) {
                        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                            xhr.setRequestHeader("X-CSRFToken", csrf_token);
                        }
                    },
                    ※ ここに後述のResponse処理が入ります。 ※
                });
            })
        }
    });
</script>

長いですが、 $('#post_image').click()まではCSRF対策です。
これに関しては、Django公式を見てください。

ポイントは二点。
$image_crop.croppie()の時にパラメータとしてbase64を指定する点と、urlにサーバサイド側で実行するViewを叩くURLをセットする点です。
このサンプルで言うとurl myapp:image_uploadの部分です。

ちなみにscriptタグの中でも、Djangoテンプレートの {%url%} 処理は使えます。
ただし、jsファイルにして外出しすると効かなくなるので注意してください。
この件についてはこちらが参考になります。

サーバサイド処理の定義

続いてDjango側の処理です。
まず、上記したPOST処理を受ける関数を用意しましょう。

views.py
def profileImageUpload(request):
    ''' プロフィール編集画面からPOSTされた画像データの保存処理 '''
    # 処理はこれから

で、この関数を叩くためのURLを用意します。

urls.py
from django.urls import path
from . import views

app_name = 'myapp'
urlpatterns = [
    path('profile/imageUpload', views.profileImageUpload, name='image_upload'),
]

続いて、保存先のモデルを用意します。
ここでは、サンプルとしてuserに1対1で紐づくProfileというクラスに保存することにしましょう。

models.py
from django.contrib.auth.models import User
from django.db import models

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
    profileImage = models.ImageField(verbose_name='プロフィール画像', blank=True)

あとは、上記のview処理を書くだけです。

サーバサイド処理の実装

さて、viewの処理です。

views.py
import base64
import cv2
import numpy as np
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from myapp.models Profile

def profileImageUpload(request):
    ''' プロフィール編集画面からPOSTされた画像データの保存処理 '''
    # POSTされたb64データを元の画像データにデコード
    image_data = base64.b64decode(request.POST.get('image').split(',')[1])
    image_binary = np.frombuffer(image_data, dtype=np.uint8)
    image = cv2.imdecode(image_binary, cv2.IMREAD_COLOR)
    temp_name = 'templateImage.png'
    cv2.imwrite(temp_name, image)

    # 画像データをリネームして移動
    dt_now = timezone.now()
    file_name = '/profileImage/' + str(request.user.pk).zfill(8) + '_' + dt_now.strftime("%Y%m%d%H%M%S") + '.png'
    os.rename(temp_name, settings.MEDIA_ROOT + file_name)

    # 保存した画像を登録
    profile = get_object_or_404(Profile, pk=request.user.profile.pk)
    profile.profileImage = file_name
    profile.save()

    data = {'imageURL' : profile.profileImage.url}
    return JsonResponse(data)

Profileモデルは、あらかじめ作成済みとしています。

大まかな処理はコメントに書いた通りなんですが、ポイントはbase64形式のデータをPNGにするまでだと思います。ここについては、こちらが参考になりました。
詳細は上記リンクを見て頂くと良いですが、以降、簡単に説明していきます。

1行目~2行目

image_data = base64.b64decode(request.POST.get('image').split(',')[1])
image_binary = np.frombuffer(image_data, dtype=np.uint8)

POSTされたデータはBase64形式なので、とりあえずこれをデコードしないと話が始まりません。
Pythonはそのためのライブラリを持っているので、それを使ってやるだけです。
ちなみにsplitはPOSTデータの前半部分にbase64,と入っているのを取り除くためです。
2行目の処理は後続でOpenCVを使うための準備になります。

3行目~5行目

image = cv2.imdecode(image_binary, cv2.IMREAD_COLOR)
temp_name = 'templateImage.png'
cv2.imwrite(temp_name, image)

デコードしたバイナリデータをPNG画像に変換します。
これにはOpenCVを使用します。変換した画像は、とりあえず適当な名前で一時保存しています。
これは、後続処理で正式な名称/置き場所に移動させます。

この時、一時的に保存する画像の名称は衝突が起きないように工夫が必要です。
このサンプルではtemplateImage.pngとしていますが、ランダムな文字列の方が良いでしょう。
私の場合は、こちらを参考に100桁のランダムな文字列を生成する関数を用意しました。
※このサンプルでは省略しています。

6行目以降

これ以降については、Djangoをチュートリアルレベルで理解できている方であれば特に問題ないと思います。
ちなみに、一次保存の処理を省いて、いきなり正式な場所に格納することも恐らく可能です。
こういう処理にしたのは実装時の都合なので、必要性はない・・・と思います(汗

レスポンス処理の実装

さて、めでたく保存が完了した後は、帰ってきた画像URLを使うだけです。

myapp_template.html
<script>
    途中省略

    // 登録ボタン押下
    $('#post_image').click(function(event){
        var csrf_token = getCookie("csrftoken");
        var rslt = window.confirm("プロフィール画像を登録してよろしいですか?");
        if (rslt) {
            $image_crop.croppie('result', 'base64').then(function(response){
                $.ajax({
                    途中省略
                    // 送信前にヘッダにcsrf_tokenを付与
                    beforeSend: function(xhr, settings) {
                        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                            xhr.setRequestHeader("X-CSRFToken", csrf_token);
                        }
                    },
                    success: function(data) {
                        $('input[type=file]').val(''); //inputファイルを空にする
                        $('#cancel').click()
                        $('#profileImage_croppie').css('display','none');
                        $('#profileImage').attr('src', data.imageURL);
                    }
                });
            })
        }
    });
</script>

AjaxのPOST処理にレスポンス処理を追記してやります。
imageタグのsrcを書き換えてやるだけですが、ついでにフォームのinput内容をクリアしてモーダルも閉じています。

最後に

最後まで見て頂いてありがとうございました。
今後も、DjangoによるWebアプリ開発をテーマに投稿を続けていく予定です。

また、別の記事でお会いできると幸いです。

参考

part1での参考サイトに加えて、下記も参考にさせて頂きました。
https://blog.narito.ninja/detail/88
https://qiita.com/TsubasaSato/items/908d4f5c241091ecbf9b

Discussion